@mtakla/cronops 0.1.1-rc6 → 0.1.1-rc7

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 CHANGED
@@ -47,6 +47,7 @@ CronOps is built and optimized to run as a Docker container itself.
47
47
 
48
48
  ```sh
49
49
  docker run \
50
+ -p 8083:8083 \
50
51
  -v ./config:/config \
51
52
  -v ./data:/io/source \
52
53
  -v ./data:/io/target \
@@ -118,12 +119,12 @@ docker compose pull && docker compose up -d
118
119
 
119
120
  in the same directory where `compose.yaml` has been created.
120
121
 
121
- ### Enable admin Web-API
122
+ ### Admin Web-API
122
123
 
123
124
  To enable **admin Web-API**, just set `CROPS_API_KEY` environment variable. Details, see [Configuration](#configuration) section below.
124
125
 
125
126
 
126
- ### Manual installation
127
+ ## Manual installation
127
128
 
128
129
  This requires [Node.js](https://nodejs.org/) (>= v24) to be installed on your server.
129
130
 
@@ -146,15 +147,15 @@ This will ...
146
147
 
147
148
  - download the latest version of dotenvx & cronops
148
149
  - load the environment settings defined in the `.env` file
149
- - start the cronops service with the loaded environment settings
150
+ - start the CronOps service with the loaded environment settings
150
151
  - create config directory in `./config` if it doesn't exist
151
152
  - create logs directory in `./logs` if it doesn't exist
152
153
 
153
154
  You can now add job configuration files to `./config/jobs` directory. Each YAML file in this directory defines a job. The server will hot reload when job files are added, modified, or removed.
154
155
 
155
- ### Use in your code
156
+ ## Use in your code
156
157
 
157
- Install CronOps in your code using npm
158
+ Install CronOps in your project using npm
158
159
 
159
160
  ```
160
161
  npm install @mtakla/cronops --save
@@ -193,8 +194,8 @@ runner.onFinished(() => {
193
194
  console.log("job finished!");
194
195
  });
195
196
 
196
- runner.onError(() => {
197
- console.log("job finished!");
197
+ runner.onError((err) => {
198
+ console.log(`job failed with ${err.message}`);
198
199
  });
199
200
 
200
201
  // finally schedule job
@@ -362,7 +363,7 @@ For jobs of action type `exec` the following environment variables are available
362
363
  | `CROPS_TEMP_DIR` | absolute path to the configured temp directory |
363
364
  | `CROPS_LOG_DIR` | absolute path to the configured log directory |
364
365
  | `CROPS_DRY_RUN` | "true", if dry_run mode is enabled |
365
- | `CROPS_VERBOSE` | "true", if dry_run mode is enabled |
366
+ | `CROPS_VERBOSE` | "true", if verbose mode is enabled |
366
367
 
367
368
 
368
369
  If the exec action is configured to run on selected `source` files:
@@ -12,4 +12,4 @@ source:
12
12
  - "**/*.log"
13
13
  target:
14
14
  dir: /outbox
15
- retention: "20s"
15
+ retention: "22s"
@@ -12,19 +12,144 @@ export declare const openapi: {
12
12
  bearerFormat: string;
13
13
  };
14
14
  };
15
+ parameters: {
16
+ JobId: {
17
+ name: string;
18
+ in: string;
19
+ required: boolean;
20
+ schema: {
21
+ type: string;
22
+ };
23
+ };
24
+ };
25
+ schemas: {
26
+ Job: {
27
+ type: string;
28
+ additionalProperties: boolean;
29
+ properties: {
30
+ id: {
31
+ type: string;
32
+ example: string;
33
+ };
34
+ cron: {
35
+ type: string;
36
+ minLength: number;
37
+ example: string;
38
+ };
39
+ action: {
40
+ type: string;
41
+ enum: string[];
42
+ example: string;
43
+ };
44
+ command: {
45
+ type: string;
46
+ };
47
+ shell: {
48
+ anyOf: ({
49
+ type: string;
50
+ minLength?: never;
51
+ } | {
52
+ type: string;
53
+ minLength: number;
54
+ })[];
55
+ };
56
+ args: {
57
+ type: string;
58
+ items: {
59
+ type: string;
60
+ minLength: number;
61
+ };
62
+ minItems: number;
63
+ };
64
+ env: {
65
+ type: string;
66
+ propertyNames: {
67
+ type: string;
68
+ pattern: string;
69
+ };
70
+ additionalProperties: {
71
+ type: string;
72
+ };
73
+ };
74
+ source: {
75
+ type: string;
76
+ additionalProperties: boolean;
77
+ properties: {
78
+ dir: {
79
+ type: string;
80
+ minLength: number;
81
+ };
82
+ includes: {
83
+ type: string;
84
+ items: {
85
+ type: string;
86
+ minLength: number;
87
+ };
88
+ minItems: number;
89
+ };
90
+ excludes: {
91
+ type: string;
92
+ items: {
93
+ type: string;
94
+ minLength: number;
95
+ };
96
+ minItems: number;
97
+ };
98
+ };
99
+ };
100
+ target: {
101
+ type: string;
102
+ additionalProperties: boolean;
103
+ properties: {
104
+ dir: {
105
+ type: string;
106
+ minLength: number;
107
+ };
108
+ archive_name: {
109
+ type: string;
110
+ minLength: number;
111
+ };
112
+ permissions: {
113
+ type: string;
114
+ additionalProperties: boolean;
115
+ properties: {
116
+ owner: {
117
+ type: string;
118
+ minLength: number;
119
+ };
120
+ file_mode: {
121
+ type: string;
122
+ minLength: number;
123
+ };
124
+ dir_mode: {
125
+ type: string;
126
+ minLength: number;
127
+ };
128
+ };
129
+ };
130
+ retention: {
131
+ type: string;
132
+ minLength: number;
133
+ };
134
+ };
135
+ };
136
+ dry_run: {
137
+ type: string;
138
+ };
139
+ verbose: {
140
+ type: string;
141
+ };
142
+ enabled: {
143
+ type: string;
144
+ };
145
+ };
146
+ };
147
+ };
15
148
  };
16
- tags: ({
149
+ tags: {
17
150
  name: string;
18
151
  description: string;
19
- externalDocs?: never;
20
- } | {
21
- name: string;
22
- description: string;
23
- externalDocs: {
24
- description: string;
25
- url: string;
26
- };
27
- })[];
152
+ }[];
28
153
  paths: {
29
154
  "/health": {
30
155
  get: {
@@ -83,17 +208,78 @@ export declare const openapi: {
83
208
  };
84
209
  };
85
210
  };
86
- "/api/jobs/trigger/{jobId}": {
87
- post: {
211
+ "/api/jobs": {
212
+ get: {
213
+ summary: string;
214
+ tags: string[];
215
+ responses: {
216
+ "200": {
217
+ description: string;
218
+ content: {
219
+ "application/json": {
220
+ schema: {
221
+ type: string;
222
+ items: {
223
+ $ref: string;
224
+ };
225
+ };
226
+ };
227
+ };
228
+ };
229
+ };
230
+ };
231
+ };
232
+ "/api/jobs/{jobId}": {
233
+ get: {
88
234
  summary: string;
89
235
  tags: string[];
90
236
  parameters: {
91
- name: string;
92
- in: string;
93
- required: boolean;
94
- schema: {
95
- type: string;
237
+ $ref: string;
238
+ }[];
239
+ responses: {
240
+ "200": {
241
+ description: string;
242
+ content: {
243
+ "application/json": {
244
+ schema: {
245
+ $ref: string;
246
+ };
247
+ };
248
+ };
249
+ };
250
+ "404": {
251
+ description: string;
252
+ };
253
+ };
254
+ };
255
+ };
256
+ "/api/status": {
257
+ get: {
258
+ summary: string;
259
+ tags: string[];
260
+ responses: {
261
+ "200": {
262
+ description: string;
263
+ content: {
264
+ "application/json": {
265
+ schema: {
266
+ type: string;
267
+ items: {
268
+ $ref: string;
269
+ };
270
+ };
271
+ };
272
+ };
96
273
  };
274
+ };
275
+ };
276
+ };
277
+ "/api/trigger/{jobId}": {
278
+ post: {
279
+ summary: string;
280
+ tags: string[];
281
+ parameters: {
282
+ $ref: string;
97
283
  }[];
98
284
  responses: {
99
285
  "200": {
@@ -123,18 +309,10 @@ export declare const openapi: {
123
309
  };
124
310
  };
125
311
  };
126
- "/api/jobs/pause/{jobId}": {
312
+ "/api/pause": {
127
313
  post: {
128
314
  summary: string;
129
315
  tags: string[];
130
- parameters: {
131
- name: string;
132
- in: string;
133
- required: boolean;
134
- schema: {
135
- type: string;
136
- };
137
- }[];
138
316
  responses: {
139
317
  "200": {
140
318
  description: string;
@@ -147,9 +325,9 @@ export declare const openapi: {
147
325
  type: string;
148
326
  example: boolean;
149
327
  };
150
- jobId: {
328
+ jobs: {
151
329
  type: string;
152
- example: string;
330
+ example: number;
153
331
  };
154
332
  };
155
333
  };
@@ -159,17 +337,12 @@ export declare const openapi: {
159
337
  };
160
338
  };
161
339
  };
162
- "/api/jobs/resume/{jobId}": {
340
+ "/api/pause/job/{jobId}": {
163
341
  post: {
164
342
  summary: string;
165
343
  tags: string[];
166
344
  parameters: {
167
- name: string;
168
- in: string;
169
- required: boolean;
170
- schema: {
171
- type: string;
172
- };
345
+ $ref: string;
173
346
  }[];
174
347
  responses: {
175
348
  "200": {
@@ -179,7 +352,7 @@ export declare const openapi: {
179
352
  schema: {
180
353
  type: string;
181
354
  properties: {
182
- resumed: {
355
+ paused: {
183
356
  type: string;
184
357
  example: boolean;
185
358
  };
@@ -195,10 +368,13 @@ export declare const openapi: {
195
368
  };
196
369
  };
197
370
  };
198
- "/api/jobs/pause/": {
371
+ "/api/resume/job/{jobId}": {
199
372
  post: {
200
373
  summary: string;
201
374
  tags: string[];
375
+ parameters: {
376
+ $ref: string;
377
+ }[];
202
378
  responses: {
203
379
  "200": {
204
380
  description: string;
@@ -207,13 +383,13 @@ export declare const openapi: {
207
383
  schema: {
208
384
  type: string;
209
385
  properties: {
210
- paused: {
386
+ resumed: {
211
387
  type: string;
212
388
  example: boolean;
213
389
  };
214
- jobs: {
390
+ jobId: {
215
391
  type: string;
216
- example: number;
392
+ example: string;
217
393
  };
218
394
  };
219
395
  };
@@ -223,7 +399,7 @@ export declare const openapi: {
223
399
  };
224
400
  };
225
401
  };
226
- "/api/jobs/resume/": {
402
+ "/api/resume": {
227
403
  post: {
228
404
  summary: string;
229
405
  tags: string[];
@@ -9,6 +9,84 @@ export const openapi = {
9
9
  bearerFormat: "hex-256",
10
10
  },
11
11
  },
12
+ parameters: {
13
+ JobId: {
14
+ name: "jobId",
15
+ in: "path",
16
+ required: true,
17
+ schema: { type: "string" },
18
+ },
19
+ },
20
+ schemas: {
21
+ Job: {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ id: { type: "string", example: "example-job" },
26
+ cron: { type: "string", minLength: 1, example: "*/5 * * * *" },
27
+ action: {
28
+ type: "string",
29
+ enum: ["exec", "call", "copy", "move", "delete", "archive"],
30
+ example: "copy",
31
+ },
32
+ command: { type: "string" },
33
+ shell: {
34
+ anyOf: [{ type: "boolean" }, { type: "string", minLength: 1 }],
35
+ },
36
+ args: {
37
+ type: "array",
38
+ items: { type: "string", minLength: 1 },
39
+ minItems: 1,
40
+ },
41
+ env: {
42
+ type: "object",
43
+ propertyNames: {
44
+ type: "string",
45
+ pattern: "^[A-Z_][A-Z0-9_]*$",
46
+ },
47
+ additionalProperties: { type: "string" },
48
+ },
49
+ source: {
50
+ type: "object",
51
+ additionalProperties: false,
52
+ properties: {
53
+ dir: { type: "string", minLength: 1 },
54
+ includes: {
55
+ type: "array",
56
+ items: { type: "string", minLength: 1 },
57
+ minItems: 1,
58
+ },
59
+ excludes: {
60
+ type: "array",
61
+ items: { type: "string", minLength: 1 },
62
+ minItems: 1,
63
+ },
64
+ },
65
+ },
66
+ target: {
67
+ type: "object",
68
+ additionalProperties: false,
69
+ properties: {
70
+ dir: { type: "string", minLength: 1 },
71
+ archive_name: { type: "string", minLength: 1 },
72
+ permissions: {
73
+ type: "object",
74
+ additionalProperties: false,
75
+ properties: {
76
+ owner: { type: "string", minLength: 1 },
77
+ file_mode: { type: "string", minLength: 1 },
78
+ dir_mode: { type: "string", minLength: 1 },
79
+ },
80
+ },
81
+ retention: { type: "string", minLength: 1 },
82
+ },
83
+ },
84
+ dry_run: { type: "boolean" },
85
+ verbose: { type: "boolean" },
86
+ enabled: { type: "boolean" },
87
+ },
88
+ },
89
+ },
12
90
  },
13
91
  tags: [
14
92
  {
@@ -17,19 +95,11 @@ export const openapi = {
17
95
  },
18
96
  {
19
97
  name: "jobs",
20
- description: "CronOps job related tasks",
98
+ description: "job related api",
21
99
  },
22
100
  {
23
- name: "admin",
24
- description: "CronOps admin tasks",
25
- },
26
- {
27
- name: "user",
28
- description: "Operations about user",
29
- externalDocs: {
30
- description: "Find out more about our store",
31
- url: "http://swagger.io",
32
- },
101
+ name: "schedule",
102
+ description: "scheduling api",
33
103
  },
34
104
  ],
35
105
  paths: {
@@ -84,18 +154,69 @@ export const openapi = {
84
154
  },
85
155
  },
86
156
  },
87
- "/api/jobs/trigger/{jobId}": {
157
+ "/api/jobs": {
158
+ get: {
159
+ summary: "Get jobs",
160
+ tags: ["jobs"],
161
+ responses: {
162
+ "200": {
163
+ description: "Array of scheduled jobs",
164
+ content: {
165
+ "application/json": {
166
+ schema: {
167
+ type: "array",
168
+ items: { $ref: "#/components/schemas/Job" },
169
+ },
170
+ },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ "/api/jobs/{jobId}": {
177
+ get: {
178
+ summary: "Get a job",
179
+ tags: ["jobs"],
180
+ parameters: [{ $ref: "#/components/parameters/JobId" }],
181
+ responses: {
182
+ "200": {
183
+ description: "Array of scheduled jobs",
184
+ content: {
185
+ "application/json": {
186
+ schema: { $ref: "#/components/schemas/Job" },
187
+ },
188
+ },
189
+ },
190
+ "404": {
191
+ description: "Job not found",
192
+ },
193
+ },
194
+ },
195
+ },
196
+ "/api/status": {
197
+ get: {
198
+ summary: "Get status",
199
+ tags: ["schedule"],
200
+ responses: {
201
+ "200": {
202
+ description: "Array of scheduled jobs",
203
+ content: {
204
+ "application/json": {
205
+ schema: {
206
+ type: "array",
207
+ items: { $ref: "#/components/schemas/Job" },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ "/api/trigger/{jobId}": {
88
216
  post: {
89
217
  summary: "Trigger a job",
90
- tags: ["jobs"],
91
- parameters: [
92
- {
93
- name: "jobId",
94
- in: "path",
95
- required: true,
96
- schema: { type: "string" },
97
- },
98
- ],
218
+ tags: ["schedule"],
219
+ parameters: [{ $ref: "#/components/parameters/JobId" }],
99
220
  responses: {
100
221
  "200": {
101
222
  description: "Triggered",
@@ -118,18 +239,10 @@ export const openapi = {
118
239
  },
119
240
  },
120
241
  },
121
- "/api/jobs/pause/{jobId}": {
242
+ "/api/pause": {
122
243
  post: {
123
- summary: "Pause a job",
124
- tags: ["jobs"],
125
- parameters: [
126
- {
127
- name: "jobId",
128
- in: "path",
129
- required: true,
130
- schema: { type: "string" },
131
- },
132
- ],
244
+ summary: "Pause jobs",
245
+ tags: ["schedule"],
133
246
  responses: {
134
247
  "200": {
135
248
  description: "Paused",
@@ -139,7 +252,7 @@ export const openapi = {
139
252
  type: "object",
140
253
  properties: {
141
254
  paused: { type: "boolean", example: true },
142
- jobId: { type: "string", example: "job-123" },
255
+ jobs: { type: "number", example: 4 },
143
256
  },
144
257
  },
145
258
  },
@@ -148,27 +261,20 @@ export const openapi = {
148
261
  },
149
262
  },
150
263
  },
151
- "/api/jobs/resume/{jobId}": {
264
+ "/api/pause/job/{jobId}": {
152
265
  post: {
153
- summary: "Resume a job",
154
- tags: ["jobs"],
155
- parameters: [
156
- {
157
- name: "jobId",
158
- in: "path",
159
- required: true,
160
- schema: { type: "string" },
161
- },
162
- ],
266
+ summary: "Pause a job",
267
+ tags: ["schedule"],
268
+ parameters: [{ $ref: "#/components/parameters/JobId" }],
163
269
  responses: {
164
270
  "200": {
165
- description: "Resumed",
271
+ description: "Paused",
166
272
  content: {
167
273
  "application/json": {
168
274
  schema: {
169
275
  type: "object",
170
276
  properties: {
171
- resumed: { type: "boolean", example: true },
277
+ paused: { type: "boolean", example: true },
172
278
  jobId: { type: "string", example: "job-123" },
173
279
  },
174
280
  },
@@ -178,20 +284,21 @@ export const openapi = {
178
284
  },
179
285
  },
180
286
  },
181
- "/api/jobs/pause/": {
287
+ "/api/resume/job/{jobId}": {
182
288
  post: {
183
- summary: "Pause all jobs",
184
- tags: ["jobs"],
289
+ summary: "Resume a paused job",
290
+ tags: ["schedule"],
291
+ parameters: [{ $ref: "#/components/parameters/JobId" }],
185
292
  responses: {
186
293
  "200": {
187
- description: "Paused",
294
+ description: "Resumed",
188
295
  content: {
189
296
  "application/json": {
190
297
  schema: {
191
298
  type: "object",
192
299
  properties: {
193
- paused: { type: "boolean", example: true },
194
- jobs: { type: "number", example: 4 },
300
+ resumed: { type: "boolean", example: true },
301
+ jobId: { type: "string", example: "job-123" },
195
302
  },
196
303
  },
197
304
  },
@@ -200,10 +307,10 @@ export const openapi = {
200
307
  },
201
308
  },
202
309
  },
203
- "/api/jobs/resume/": {
310
+ "/api/resume": {
204
311
  post: {
205
- summary: "Resume all jobs",
206
- tags: ["jobs"],
312
+ summary: "Resume paused jobs",
313
+ tags: ["schedule"],
207
314
  responses: {
208
315
  "200": {
209
316
  description: "Resumed",
@@ -5,7 +5,7 @@ import { openapi } from "./openapi.js";
5
5
  const app = Fastify();
6
6
  const port = Number(process.env[ENV.PORT] ?? 8118);
7
7
  const host = process.env[ENV.HOST] ?? "127.0.0.1";
8
- const baseUrl = process.env[ENV.BASE_URL] ?? "http://127.0.0.1:8118";
8
+ const baseUrl = process.env[ENV.BASE_URL] ?? `http://127.0.0.1:${port}`;
9
9
  const apiKey = process.env[ENV.API_KEY];
10
10
  app.addHook("preHandler", async (request, reply) => {
11
11
  if (request.method === "OPTIONS" || !request.url.startsWith("/api"))
@@ -38,7 +38,33 @@ app.get("/health", async (request, reply) => {
38
38
  const jobs = jobScheduler.getScheduledJobs();
39
39
  reply.code(200).send({ status: "ok", active_jobs: jobs.length });
40
40
  });
41
- app.post("/api/jobs/trigger/:jobId", async (request, reply) => {
41
+ app.get("/api/jobs", async (request) => {
42
+ const jobScheduler = request.server.scheduler;
43
+ return jobScheduler.getScheduledJobs();
44
+ });
45
+ app.get("/api/jobs/:jobId", async (request, reply) => {
46
+ const { jobId } = request.params;
47
+ const jobScheduler = request.server.scheduler;
48
+ if (!jobScheduler.isJobScheduled(jobId)) {
49
+ return reply.code(404).send();
50
+ }
51
+ return jobScheduler.getJob(jobId);
52
+ });
53
+ app.get("/api/status", async (request) => {
54
+ const jobScheduler = request.server.scheduler;
55
+ return jobScheduler.getScheduledJobsInfo();
56
+ });
57
+ app.post("/api/pause", async (request) => {
58
+ const jobScheduler = request.server.scheduler;
59
+ await jobScheduler.pauseScheduling();
60
+ return { paused: true };
61
+ });
62
+ app.post("/api/resume", async (request) => {
63
+ const jobScheduler = request.server.scheduler;
64
+ await jobScheduler.resumeScheduling();
65
+ return { resumed: true };
66
+ });
67
+ app.post("/api/trigger/:jobId", async (request, reply) => {
42
68
  const { jobId } = request.params;
43
69
  const jobScheduler = request.server.scheduler;
44
70
  if (!jobScheduler.isJobScheduled(jobId)) {
@@ -141,12 +141,12 @@ export class AbstractHandler {
141
141
  await Promise.all(targetScanPromises);
142
142
  fileHistory.cleanup();
143
143
  }
144
- async setTargetFilePermissions(destPath, perms, isDir = false) {
144
+ async setTargetFilePermissions(destFile, perms, isDir = false) {
145
145
  const mode = isDir ? perms.dirMode : perms.fileMode;
146
146
  if (perms.uid >= 0 && perms.gid >= 0)
147
- await fsx.chown(destPath, perms.uid, perms.gid);
147
+ await fsx.chown(destFile, perms.uid, perms.gid);
148
148
  if (mode >= 0)
149
- await fsx.chmod(destPath, mode);
149
+ await fsx.chmod(destFile, mode);
150
150
  }
151
151
  getFolderPermissionPromises(ctx, dirPath, folderPromises) {
152
152
  if (dirPath.length < ctx.targetDir.length || folderPromises.has(dirPath))
package/dist/server.js CHANGED
@@ -34,18 +34,25 @@ export async function start() {
34
34
  console.log(`🔴 Error loading job '${entry}'. ${message}`);
35
35
  });
36
36
  jobLoader.onJobLoaded((job) => {
37
- if (job.enabled !== false)
38
- jobScheduler.scheduleJob(job);
37
+ jobScheduler.scheduleJob(job);
39
38
  });
40
39
  jobLoader.onJobDeleted((jobId) => {
41
40
  jobScheduler.unscheduleJob(jobId);
42
41
  });
43
42
  jobScheduler.onChanged((isReload) => {
44
- const jobs = jobScheduler.getScheduledJobs();
45
- console.log(`\nJob config ${isReload ? "changed" : "loaded"} (${plural(jobs.length, "active job")})`);
43
+ const jobs = jobScheduler.getScheduledJobsInfo();
44
+ console.log(`\nJob config ${isReload ? "changed" : "loaded"}`);
46
45
  for (const job of jobs) {
47
- console.log(` 🕔 [${job.id}] scheduled (${chalk.greenBright(job.cron)})${job.dry_run ? " 👋 DRY-RUN mode!" : ""}`);
46
+ if (job.status !== "paused")
47
+ console.log(` 🕔 [${job.id}] scheduled (${chalk.greenBright(job.cron)})${job.dry_run ? " 👋 DRY-RUN mode!" : ""}`);
48
48
  }
49
+ for (const job of jobs) {
50
+ if (job.status === "paused")
51
+ console.log(` ⚫ [${job.id}] inactive`);
52
+ }
53
+ });
54
+ jobScheduler.onJobExecute((job) => {
55
+ console.error(`[${job.id}] triggered manually`);
49
56
  });
50
57
  jobScheduler.onJobError((job, err) => {
51
58
  console.error(chalk.red(`[${job.id}] ERROR ${err.message}`));
@@ -1,17 +1,26 @@
1
1
  import { type ScheduledTask } from "node-cron";
2
2
  import { EventEmitter } from "node:events";
3
- import type { Task } from "../types/Task.types.js";
3
+ import type { Task, TaskInfo } from "../types/Task.types.js";
4
4
  export declare abstract class AbstractTask<T> implements Task {
5
5
  protected cronTask: ScheduledTask;
6
6
  protected events: EventEmitter<any>;
7
7
  protected errorCount: number;
8
+ private runCount;
9
+ private isScheduled;
8
10
  private isRunning;
11
+ private isPaused;
12
+ private lastRun?;
13
+ private lastDuration?;
9
14
  constructor(cronStr?: string);
10
15
  protected abstract run(): Promise<T>;
11
16
  schedule(runImmediately?: boolean): void;
12
17
  unschedule(): void;
13
- execute(cb?: (result: T) => void): void;
18
+ pause(): void;
19
+ resume(): void;
20
+ getInfo(): TaskInfo;
21
+ execute<T>(): Promise<T>;
14
22
  onScheduled(cb: () => void): void;
23
+ onExecute(cb: () => void): void;
15
24
  onStarted(cb: () => void): void;
16
25
  onFinished<T>(cb: (result: T) => void): void;
17
26
  onError(cb: (error: Error) => void): void;
@@ -6,20 +6,29 @@ export class AbstractTask {
6
6
  cronTask;
7
7
  events = new EventEmitter();
8
8
  errorCount = 0;
9
+ runCount = 0;
10
+ isScheduled = false;
9
11
  isRunning = false;
12
+ isPaused = false;
13
+ lastRun;
14
+ lastDuration;
10
15
  constructor(cronStr = "* * * * *") {
11
16
  const asyncRunner = async () => {
12
- if (!this.isRunning) {
17
+ if (!this.isRunning && !this.isPaused) {
18
+ this.runCount++;
13
19
  this.isRunning = true;
20
+ this.lastRun = Date.now();
14
21
  try {
15
22
  this.events.emit("started");
16
23
  this.events.emit("finished", await this.run());
17
24
  }
18
25
  catch (err) {
26
+ this.errorCount++;
19
27
  this.events.emit("error", err instanceof Error ? err : new Error(String(err)));
20
28
  }
21
29
  finally {
22
30
  this.isRunning = false;
31
+ this.lastDuration = Date.now() - this.lastRun;
23
32
  }
24
33
  }
25
34
  };
@@ -30,26 +39,45 @@ export class AbstractTask {
30
39
  }
31
40
  schedule(runImmediately = false) {
32
41
  if (runImmediately)
33
- this.cronTask.once("task:started", () => this.execute());
42
+ this.cronTask.once("task:started", () => this.cronTask.execute());
34
43
  this.cronTask.start();
44
+ this.isScheduled = true;
35
45
  }
36
46
  unschedule() {
37
47
  this.events.removeAllListeners();
38
48
  this.cronTask.destroy();
49
+ this.isScheduled = false;
39
50
  }
40
- execute(cb) {
51
+ pause() {
52
+ this.isPaused = true;
53
+ }
54
+ resume() {
55
+ this.isPaused = false;
56
+ }
57
+ getInfo() {
58
+ return {
59
+ status: this.isRunning ? "running" : this.isPaused ? "paused" : this.isScheduled ? "scheduled" : "unscheduled",
60
+ runCount: this.runCount,
61
+ errorCount: this.errorCount,
62
+ lastRun: this.lastRun,
63
+ lastDuration: this.lastDuration,
64
+ };
65
+ }
66
+ execute() {
41
67
  const status = this.cronTask.getStatus();
42
68
  if (status === "destroyed")
43
69
  throw new Error("Invalid task state (destroyed)");
44
70
  if (status === "running" || this.isRunning)
45
71
  throw new Error("Invalid task state (running)");
46
- if (cb)
47
- this.events.once("finished", cb);
48
- this.cronTask.execute();
72
+ this.events.emit("execute");
73
+ return this.cronTask.execute();
49
74
  }
50
75
  onScheduled(cb) {
51
76
  this.cronTask.on("task:started", cb);
52
77
  }
78
+ onExecute(cb) {
79
+ this.events.on("execute", cb);
80
+ }
53
81
  onStarted(cb) {
54
82
  this.events.on("started", cb);
55
83
  }
@@ -57,7 +85,6 @@ export class AbstractTask {
57
85
  this.events.on("finished", cb);
58
86
  }
59
87
  onError(cb) {
60
- this.errorCount++;
61
88
  this.events.on("error", (error) => cb(error));
62
89
  }
63
90
  async gracefulTerminate(timeout = 500) {
@@ -1,5 +1,4 @@
1
1
  import { AbstractTask } from "./AbstractTask.js";
2
- import { JobRunnerResult } from "../models/JobRunnerResult.js";
3
2
  import type { JobModel } from "../models/JobModel.js";
4
3
  import type { JobRunnerSetup } from "../models/JobRunnerSetup.js";
5
4
  import type { RunnerResult, FileHistory } from "../types/Task.types.js";
@@ -9,8 +8,8 @@ export declare class JobRunner extends AbstractTask<RunnerResult> {
9
8
  setup: JobRunnerSetup;
10
9
  constructor(job: JobModel, setup: JobRunnerSetup);
11
10
  onActivity(cb: (action: string, path: string, count: number) => void): void;
12
- runJob(): Promise<JobRunnerResult>;
13
- protected run(): Promise<JobRunnerResult>;
11
+ runJob(): Promise<RunnerResult>;
12
+ protected run(): Promise<RunnerResult>;
14
13
  protected loadFileHistory(job: Job): Promise<FileHistory>;
15
14
  protected saveFileHistory(job: Job, fileHistory: FileHistory): Promise<void>;
16
15
  protected initLog(job: Job): number;
@@ -4,7 +4,6 @@ import { ensureDir, moveSync, readJSON, writeJSON } from "fs-extra/esm";
4
4
  import { AbstractTask } from "./AbstractTask.js";
5
5
  import { JobRunnerContext } from "../models/JobRunnerContext.js";
6
6
  import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
7
- import { JobRunnerResult } from "../models/JobRunnerResult.js";
8
7
  import { FileHistoryModel } from "../models/FileHistoryModel.js";
9
8
  export class JobRunner extends AbstractTask {
10
9
  job;
@@ -22,10 +21,8 @@ export class JobRunner extends AbstractTask {
22
21
  }
23
22
  async run() {
24
23
  const { setup, job, events } = this;
25
- if (job.enabled === false)
26
- return new JobRunnerResult();
27
- if (this.errorCount >= 25) {
28
- job.enabled = false;
24
+ if (this.errorCount >= 31) {
25
+ this.pause();
29
26
  throw new Error("Too many errors. Job execution disabled!");
30
27
  }
31
28
  await ensureDir(setup.logDir);
@@ -2,8 +2,9 @@ import { JobRunner } from "./JobRunner.js";
2
2
  import { JobRunnerSetup } from "../models/JobRunnerSetup.js";
3
3
  import { AbstractTask } from "./AbstractTask.js";
4
4
  import type { Job } from "../types/Config.types.js";
5
- import type { RunnerResult } from "../types/Task.types.js";
5
+ import type { RunnerResult, TaskInfo } from "../types/Task.types.js";
6
6
  import type { RunnerOptions } from "../types/Options.types.js";
7
+ type SchedulingInfo = Job & TaskInfo;
7
8
  export declare class JobScheduler extends AbstractTask<void> {
8
9
  protected runnerSetup: JobRunnerSetup;
9
10
  protected runnerMap: Map<string, JobRunner>;
@@ -13,19 +14,29 @@ export declare class JobScheduler extends AbstractTask<void> {
13
14
  get scheduledJobs(): number;
14
15
  get tempDir(): string;
15
16
  protected run(): Promise<void>;
17
+ pauseAll(): void;
18
+ resumeAll(): void;
16
19
  unscheduleAll(): void;
17
20
  scheduleJobs(jobs: Job[], cb?: (count: number) => void): void;
18
- scheduleJob(job: Job, defaults?: {}): void;
21
+ scheduleJob(job: Job): void;
19
22
  unscheduleJob(jobId: string): void;
20
23
  isJobScheduled(jobId: string): boolean;
24
+ getJob(jobId: string): Job | undefined;
21
25
  executeJob(jobId: string): void;
22
26
  validateJob(job: Job): void;
27
+ pauseScheduling(): void;
28
+ resumeScheduling(): void;
29
+ pauseJobScheduling(jobId: string): void;
30
+ resumeJobScheduling(jobId: string): void;
23
31
  getScheduledJobs(): Job[];
32
+ getScheduledJobsInfo(): SchedulingInfo[];
24
33
  gracefulTerminate(timeout?: number): Promise<void>;
25
34
  onChanged(cb: (initialConfig: boolean) => void): void;
26
35
  onJobScheduled(cb: (job: Job, rescheduled: boolean) => void): void;
36
+ onJobExecute(cb: (job: Job) => void): void;
27
37
  onJobStarted(cb: (job: Job) => void): void;
28
38
  onJobFinished(cb: (job: Job, stat: RunnerResult) => void): void;
29
39
  onJobActivity(cb: (job: Job, activity: string, path: string, count: number) => void): void;
30
40
  onJobError(cb: (job: Job, err: Error) => void): void;
31
41
  }
42
+ export {};
@@ -2,7 +2,6 @@ import { JobRunner } from "./JobRunner.js";
2
2
  import { JobModel } from "../models/JobModel.js";
3
3
  import { JobRunnerSetup } from "../models/JobRunnerSetup.js";
4
4
  import { AbstractTask } from "./AbstractTask.js";
5
- import { JobError } from "../errors/JobError.js";
6
5
  export class JobScheduler extends AbstractTask {
7
6
  runnerSetup;
8
7
  runnerMap;
@@ -26,6 +25,14 @@ export class JobScheduler extends AbstractTask {
26
25
  this.isReload = true;
27
26
  }
28
27
  }
28
+ pauseAll() {
29
+ for (const runner of this.runnerMap.values())
30
+ runner.resume();
31
+ }
32
+ resumeAll() {
33
+ for (const runner of this.runnerMap.values())
34
+ runner.resume();
35
+ }
29
36
  unscheduleAll() {
30
37
  for (const runner of this.runnerMap.values())
31
38
  runner.unschedule();
@@ -39,23 +46,24 @@ export class JobScheduler extends AbstractTask {
39
46
  if (cb)
40
47
  cb(this.runnerMap.size);
41
48
  }
42
- scheduleJob(job, defaults = {}) {
49
+ scheduleJob(job) {
43
50
  let rescheduled = false;
44
- if (job.enabled === false)
45
- JobError.throw(job.id, `Cannot schedule disabled job [${job.id}]!`);
46
51
  this.runnerSetup.validateJob(job);
47
52
  if (this.runnerMap.has(job.id)) {
48
53
  this.runnerMap.get(job.id)?.unschedule();
49
54
  rescheduled = true;
50
55
  }
51
- const task = new JobRunner(new JobModel(job, defaults), this.runnerSetup);
56
+ const task = new JobRunner(new JobModel(job), this.runnerSetup);
52
57
  this.runnerMap.set(job.id, task);
53
58
  task.onScheduled(() => this.events.emit("job-scheduled", job, rescheduled));
59
+ task.onExecute(() => this.events.emit("job-execute", job));
54
60
  task.onStarted(() => this.events.emit("job-started", job));
55
61
  task.onFinished((stat) => this.events.emit("job-finished", job, stat));
56
62
  task.onActivity((activity, path, count) => this.events.emit("job-activity", job, activity, path, count));
57
63
  task.onError((err) => this.events.emit("job-error", job, err));
58
64
  task.schedule();
65
+ if (job.enabled === false)
66
+ task.pause();
59
67
  this.changed = true;
60
68
  }
61
69
  unscheduleJob(jobId) {
@@ -69,6 +77,9 @@ export class JobScheduler extends AbstractTask {
69
77
  isJobScheduled(jobId) {
70
78
  return this.runnerMap.has(jobId);
71
79
  }
80
+ getJob(jobId) {
81
+ return this.runnerMap.get(jobId)?.job;
82
+ }
72
83
  executeJob(jobId) {
73
84
  if (!this.runnerMap.has(jobId))
74
85
  throw new Error(`Unknown job [${jobId}]!`);
@@ -77,9 +88,32 @@ export class JobScheduler extends AbstractTask {
77
88
  validateJob(job) {
78
89
  this.runnerSetup.validateJob(job);
79
90
  }
91
+ pauseScheduling() {
92
+ for (const task of this.runnerMap.values())
93
+ this.pauseJobScheduling(task.job.id);
94
+ this.changed = true;
95
+ }
96
+ resumeScheduling() {
97
+ for (const task of this.runnerMap.values())
98
+ this.resumeJobScheduling(task.job.id);
99
+ this.changed = true;
100
+ }
101
+ pauseJobScheduling(jobId) {
102
+ const task = this.runnerMap.get(jobId);
103
+ if (task)
104
+ task.pause();
105
+ }
106
+ resumeJobScheduling(jobId) {
107
+ const task = this.runnerMap.get(jobId);
108
+ if (task && task.job.enabled !== false)
109
+ task.resume();
110
+ }
80
111
  getScheduledJobs() {
81
112
  return Array.from(this.runnerMap.values()).map((runner) => runner.job);
82
113
  }
114
+ getScheduledJobsInfo() {
115
+ return Array.from(this.runnerMap.values()).map((runner) => Object.assign({}, runner.job, runner.getInfo()));
116
+ }
83
117
  async gracefulTerminate(timeout = 1000) {
84
118
  const jobRunnerTasks = [...this.runnerMap.values()];
85
119
  this.unscheduleAll();
@@ -93,6 +127,9 @@ export class JobScheduler extends AbstractTask {
93
127
  onJobScheduled(cb) {
94
128
  this.events.on("job-scheduled", cb);
95
129
  }
130
+ onJobExecute(cb) {
131
+ this.events.on("job-execute", cb);
132
+ }
96
133
  onJobStarted(cb) {
97
134
  this.events.on("job-started", cb);
98
135
  }
@@ -2,7 +2,14 @@ import { z } from "zod";
2
2
  export declare const JobSchema: z.ZodObject<{
3
3
  id: z.ZodOptional<z.ZodString>;
4
4
  cron: z.ZodOptional<z.ZodString>;
5
- action: z.ZodLiteral<"exec" | "call" | "copy" | "move" | "delete" | "archive">;
5
+ action: z.ZodEnum<{
6
+ exec: "exec";
7
+ call: "call";
8
+ copy: "copy";
9
+ move: "move";
10
+ delete: "delete";
11
+ archive: "archive";
12
+ }>;
6
13
  command: z.ZodOptional<z.ZodString>;
7
14
  shell: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodString]>>;
8
15
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export const JobSchema = z.strictObject({
3
3
  id: z.string().optional(),
4
4
  cron: z.string().min(1).optional(),
5
- action: z.literal(["exec", "call", "copy", "move", "delete", "archive"]),
5
+ action: z.enum(["exec", "call", "copy", "move", "delete", "archive"]),
6
6
  command: z.string().optional(),
7
7
  shell: z.boolean().or(z.string().min(1)).optional(),
8
8
  args: z.array(z.string().min(1)).min(1).optional(),
@@ -13,6 +13,13 @@ export type RunnerResult = {
13
13
  endTime: number;
14
14
  durationMs: number;
15
15
  };
16
+ export type TaskInfo = {
17
+ status: "unscheduled" | "scheduled" | "running" | "paused";
18
+ runCount: number;
19
+ errorCount: number;
20
+ lastRun: number | undefined;
21
+ lastDuration: number | undefined;
22
+ };
16
23
  export type SourceFile = {
17
24
  sourceEntry: string;
18
25
  sourcePath: string;
@@ -36,7 +43,7 @@ export type FileHistory = {
36
43
  export interface Task {
37
44
  schedule(runImmediately?: boolean): void;
38
45
  unschedule(): void;
39
- execute(cb: () => void): void;
46
+ execute<T>(): Promise<T>;
40
47
  onScheduled(cb: () => void): void;
41
48
  onStarted(cb: () => void): void;
42
49
  onFinished<T>(cb: (result: T) => void): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtakla/cronops",
3
- "version": "0.1.1-rc6",
3
+ "version": "0.1.1-rc7",
4
4
  "description": "Automated cross-container file lifecycle management with cron scheduling",
5
5
  "type": "module",
6
6
  "author": "mtakla@nevereven",