@luisrodrigues/nestjs-scheduler-dashboard 0.0.2 → 0.0.4
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 +44 -69
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +9 -14
- package/dist/{jobs.controller.d.ts → dashboard.controller.d.ts} +5 -3
- package/dist/{jobs.controller.js → dashboard.controller.js} +21 -31
- package/dist/{jobs-tracker.service.d.ts → dashboard.module.d.ts} +4 -3
- package/dist/{jobs-tracker.service.js → dashboard.module.js} +25 -15
- package/dist/decorators/job-concurrency.d.ts +11 -11
- package/dist/decorators/job-concurrency.js +38 -43
- package/dist/decorators/track-job.decorator.js +2 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -19
- package/dist/jobs.service.js +1 -1
- package/dist/scheduler-dash.context.d.ts +6 -7
- package/dist/scheduler-dash.context.js +15 -6
- package/dist/scheduler-dash.options.d.ts +0 -1
- package/dist/scheduler-dash.schema.d.ts +0 -1
- package/dist/scheduler-dash.schema.js +0 -4
- package/dist/standalone-server.d.ts +1 -2
- package/dist/standalone-server.js +32 -64
- package/dist/ui/dashboard.d.ts +1 -1
- package/dist/ui/dashboard.js +1 -1
- package/package.json +13 -13
- package/ui/assets/index-BerSFPJX.css +1 -0
- package/ui/assets/index-C5PR13H0.js +235 -0
- package/ui/index.html +21 -0
- package/dist/basic-auth.guard.d.ts +0 -7
- package/dist/basic-auth.guard.js +0 -44
- package/dist/dashboard.server.d.ts +0 -4
- package/dist/dashboard.server.js +0 -63
- package/dist/embedded-server.d.ts +0 -4
- package/dist/embedded-server.js +0 -47
- package/dist/scheduler-dash.module.d.ts +0 -5
- package/dist/scheduler-dash.module.js +0 -39
package/README.md
CHANGED
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
A plug-and-play dashboard for [`@nestjs/schedule`](https://docs.nestjs.com/techniques/task-scheduling). Visualize cron job executions, track history and metrics, trigger jobs manually, and stop running executions — all from an embedded UI with zero external dependencies.
|
|
4
4
|
|
|
5
|
+
The dashboard runs on its own dedicated port (default **3636**), completely isolated from your main application.
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
|
-
- Embedded UI served directly from your NestJS app (no separate frontend server)
|
|
10
11
|
- Execution history per job with status, duration, and error details
|
|
11
12
|
- Persistent metrics: total runs, failed runs, average duration — independent of history retention
|
|
12
13
|
- Manual job triggering and execution stop from the UI
|
|
13
|
-
- Concurrency control: limit how many jobs run simultaneously with
|
|
14
|
-
- No-overlap mode: skip
|
|
14
|
+
- Concurrency control: limit how many jobs run simultaneously, with automatic queuing
|
|
15
|
+
- No-overlap mode: skip a job if it is already running
|
|
15
16
|
- Optional HTTP Basic Auth to protect the dashboard
|
|
16
17
|
- Light / dark mode
|
|
17
|
-
-
|
|
18
|
+
- Zero external runtime dependencies — served from a single self-contained HTML file
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -29,14 +30,14 @@ pnpm add @luisrodrigues/nestjs-scheduler-dashboard
|
|
|
29
30
|
**Peer dependencies** (install if not already present):
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
npm install @nestjs/common @nestjs/schedule
|
|
33
|
+
npm install @nestjs/common @nestjs/core @nestjs/schedule
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
37
38
|
## Quick start
|
|
38
39
|
|
|
39
|
-
### 1.
|
|
40
|
+
### 1. Call `setupSchedulerDash` in `main.ts`
|
|
40
41
|
|
|
41
42
|
```ts
|
|
42
43
|
import { NestFactory } from '@nestjs/core';
|
|
@@ -46,16 +47,17 @@ import { setupSchedulerDash } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
|
46
47
|
async function bootstrap() {
|
|
47
48
|
const app = await NestFactory.create(AppModule);
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// Must be called BEFORE app.listen() so storage is ready before cron jobs start
|
|
51
|
+
await setupSchedulerDash(app, { port: 3636 });
|
|
50
52
|
|
|
51
53
|
await app.listen(3000);
|
|
54
|
+
console.log('App running at http://localhost:3000');
|
|
55
|
+
console.log('Dashboard at http://localhost:3636');
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
bootstrap();
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
The dashboard is now available at `http://localhost:3000/_jobs`.
|
|
58
|
-
|
|
59
61
|
### 2. Decorate your jobs
|
|
60
62
|
|
|
61
63
|
Replace `@Cron` with `@TrackJob` — it accepts the same arguments:
|
|
@@ -74,24 +76,25 @@ export class ReportJob {
|
|
|
74
76
|
}
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
That's it. Open `http://localhost:3636` to see the dashboard.
|
|
80
|
+
|
|
77
81
|
---
|
|
78
82
|
|
|
79
83
|
## Configuration
|
|
80
84
|
|
|
81
|
-
`setupSchedulerDash(app, options?)` accepts
|
|
85
|
+
`setupSchedulerDash(app, options?)` accepts:
|
|
82
86
|
|
|
83
87
|
| Option | Type | Default | Description |
|
|
84
88
|
|---|---|---|---|
|
|
89
|
+
| `port` | `number` | `3636` | Port for the dashboard HTTP server |
|
|
85
90
|
| `storage` | `Storage` | `new MemoryStorage()` | Storage backend for execution history and metrics |
|
|
86
|
-
| `
|
|
87
|
-
| `port` | `number` | — | When set, serves the dashboard on a separate HTTP server instead of mounting on the main app |
|
|
88
|
-
| `maxConcurrent` | `number` | — | Maximum number of `@TrackJob` jobs that can run at the same time. Excess jobs are queued |
|
|
91
|
+
| `maxConcurrent` | `number` | — | Maximum number of jobs that can run simultaneously. Excess jobs are queued |
|
|
89
92
|
| `noOverlap` | `boolean` | `false` | Globally prevent a job from starting if it is already running |
|
|
90
|
-
| `auth` | `{ username, password }` | — | Protect
|
|
93
|
+
| `auth` | `{ username, password }` | — | Protect the dashboard with HTTP Basic Auth |
|
|
91
94
|
|
|
92
95
|
### `storage`
|
|
93
96
|
|
|
94
|
-
The
|
|
97
|
+
The default `MemoryStorage` keeps everything in-process. You can limit how many history entries are kept per job:
|
|
95
98
|
|
|
96
99
|
```ts
|
|
97
100
|
import { MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
@@ -103,7 +106,7 @@ await setupSchedulerDash(app, {
|
|
|
103
106
|
});
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
> **Metrics are independent of `historyRetention`.** Even
|
|
109
|
+
> **Metrics are independent of `historyRetention`.** Even after old history entries are trimmed, the counters for total runs, failed runs, and average duration keep accumulating.
|
|
107
110
|
|
|
108
111
|
To use a custom storage backend, extend the abstract `Storage` class:
|
|
109
112
|
|
|
@@ -124,42 +127,19 @@ export class RedisStorage extends Storage {
|
|
|
124
127
|
}
|
|
125
128
|
```
|
|
126
129
|
|
|
127
|
-
### `basePath`
|
|
128
|
-
|
|
129
|
-
Changes the URL where the dashboard is served. Must contain only alphanumeric characters, hyphens, underscores, or slashes.
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
await setupSchedulerDash(app, {
|
|
133
|
-
basePath: 'admin/scheduler',
|
|
134
|
-
// dashboard → http://localhost:3000/admin/scheduler
|
|
135
|
-
});
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### `port`
|
|
139
|
-
|
|
140
|
-
Serve the dashboard on a completely separate HTTP server, isolated from your main application.
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
await setupSchedulerDash(app, {
|
|
144
|
-
port: 3001,
|
|
145
|
-
// dashboard → http://localhost:3001/_jobs
|
|
146
|
-
// main app → http://localhost:3000
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
130
|
### `maxConcurrent`
|
|
151
131
|
|
|
152
|
-
Limits
|
|
132
|
+
Limits how many `@TrackJob` jobs can run simultaneously across the entire application. Jobs that exceed the limit are saved to storage with status `"queued"` and run in FIFO order as slots free up.
|
|
153
133
|
|
|
154
134
|
```ts
|
|
155
135
|
await setupSchedulerDash(app, {
|
|
156
|
-
maxConcurrent: 5,
|
|
136
|
+
maxConcurrent: 5,
|
|
157
137
|
});
|
|
158
138
|
```
|
|
159
139
|
|
|
160
140
|
### `noOverlap`
|
|
161
141
|
|
|
162
|
-
Prevents a job from firing again if
|
|
142
|
+
Prevents a job from firing again if it is still running. Applies globally to all `@TrackJob` methods.
|
|
163
143
|
|
|
164
144
|
```ts
|
|
165
145
|
await setupSchedulerDash(app, {
|
|
@@ -167,7 +147,7 @@ await setupSchedulerDash(app, {
|
|
|
167
147
|
});
|
|
168
148
|
```
|
|
169
149
|
|
|
170
|
-
Can also be
|
|
150
|
+
Can also be overridden per job in the decorator:
|
|
171
151
|
|
|
172
152
|
```ts
|
|
173
153
|
@TrackJob(CronExpression.EVERY_MINUTE, { name: 'sync', noOverlap: true })
|
|
@@ -176,13 +156,11 @@ async sync() { /* ... */ }
|
|
|
176
156
|
|
|
177
157
|
### `auth`
|
|
178
158
|
|
|
179
|
-
Protect the dashboard with HTTP Basic Auth.
|
|
180
|
-
|
|
181
159
|
```ts
|
|
182
160
|
await setupSchedulerDash(app, {
|
|
183
161
|
auth: {
|
|
184
|
-
username: 'admin',
|
|
185
|
-
password: '
|
|
162
|
+
username: process.env.DASH_USER ?? 'admin',
|
|
163
|
+
password: process.env.DASH_PASS ?? 'secret',
|
|
186
164
|
},
|
|
187
165
|
});
|
|
188
166
|
```
|
|
@@ -191,7 +169,7 @@ await setupSchedulerDash(app, {
|
|
|
191
169
|
|
|
192
170
|
## `@TrackJob` decorator
|
|
193
171
|
|
|
194
|
-
|
|
172
|
+
Drop-in replacement for `@Cron`. Accepts all the same options plus `noOverlap`.
|
|
195
173
|
|
|
196
174
|
```ts
|
|
197
175
|
@TrackJob(cronTime, options?)
|
|
@@ -200,9 +178,9 @@ await setupSchedulerDash(app, {
|
|
|
200
178
|
| Argument | Type | Description |
|
|
201
179
|
|---|---|---|
|
|
202
180
|
| `cronTime` | `string \| CronExpression` | Cron expression or `CronExpression` enum value |
|
|
203
|
-
| `options.name` | `string` |
|
|
204
|
-
| `options.noOverlap` | `boolean` | Skip this job if it is already running. Overrides the global
|
|
205
|
-
| `options.*` | — | All other [`CronOptions`](https://docs.nestjs.com/techniques/task-scheduling)
|
|
181
|
+
| `options.name` | `string` | Job name shown in the dashboard. Defaults to `ClassName.methodName` |
|
|
182
|
+
| `options.noOverlap` | `boolean` | Skip this job if it is already running. Overrides the global setting |
|
|
183
|
+
| `options.*` | — | All other [`CronOptions`](https://docs.nestjs.com/techniques/task-scheduling) are passed through |
|
|
206
184
|
|
|
207
185
|
```ts
|
|
208
186
|
@TrackJob('0 */6 * * *', {
|
|
@@ -217,22 +195,31 @@ async cleanup() {
|
|
|
217
195
|
|
|
218
196
|
---
|
|
219
197
|
|
|
220
|
-
##
|
|
198
|
+
## API
|
|
199
|
+
|
|
200
|
+
The dashboard exposes a small REST API on the same port as the UI.
|
|
201
|
+
|
|
202
|
+
| Method | Path | Description |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `GET` | `/api` | Returns all jobs with history and metrics |
|
|
205
|
+
| `POST` | `/api/:name/trigger` | Manually trigger a cron job by name |
|
|
206
|
+
| `POST` | `/api/executions/:id/stop` | Stop a running or queued execution by ID |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Full example
|
|
221
211
|
|
|
222
212
|
```ts
|
|
223
213
|
import { NestFactory } from '@nestjs/core';
|
|
224
214
|
import { AppModule } from './app.module';
|
|
225
|
-
import {
|
|
226
|
-
setupSchedulerDash,
|
|
227
|
-
MemoryStorage,
|
|
228
|
-
} from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
215
|
+
import { setupSchedulerDash, MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
229
216
|
|
|
230
217
|
async function bootstrap() {
|
|
231
218
|
const app = await NestFactory.create(AppModule);
|
|
232
219
|
|
|
233
220
|
await setupSchedulerDash(app, {
|
|
221
|
+
port: 3636,
|
|
234
222
|
storage: new MemoryStorage({ historyRetention: 100 }),
|
|
235
|
-
basePath: 'scheduler',
|
|
236
223
|
maxConcurrent: 3,
|
|
237
224
|
noOverlap: true,
|
|
238
225
|
auth: {
|
|
@@ -249,18 +236,6 @@ bootstrap();
|
|
|
249
236
|
|
|
250
237
|
---
|
|
251
238
|
|
|
252
|
-
## API
|
|
253
|
-
|
|
254
|
-
The dashboard exposes a small REST API used by the UI. You can also call it directly.
|
|
255
|
-
|
|
256
|
-
| Method | Path | Description |
|
|
257
|
-
|---|---|---|
|
|
258
|
-
| `GET` | `/{basePath}/api` | Returns all jobs with history and metrics |
|
|
259
|
-
| `POST` | `/{basePath}/api/:name/trigger` | Manually trigger a cron job by name |
|
|
260
|
-
| `POST` | `/{basePath}/api/executions/:id/stop` | Stop a running or queued execution by ID |
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
239
|
## License
|
|
265
240
|
|
|
266
241
|
MIT
|
package/dist/auth.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ import { IncomingMessage, ServerResponse } from 'http';
|
|
|
2
2
|
import { SchedulerDashAuth } from './scheduler-dash.options';
|
|
3
3
|
export declare function checkBasicAuth(req: IncomingMessage, auth: SchedulerDashAuth): boolean;
|
|
4
4
|
export declare function rejectUnauthorized(res: ServerResponse): void;
|
|
5
|
-
export declare function createAuthGuard(auth: SchedulerDashAuth | undefined): (
|
|
5
|
+
export declare function createAuthGuard(auth: SchedulerDashAuth | undefined): (_req: any, _res: any, next: any) => any;
|
package/dist/auth.js
CHANGED
|
@@ -5,27 +5,22 @@ exports.rejectUnauthorized = rejectUnauthorized;
|
|
|
5
5
|
exports.createAuthGuard = createAuthGuard;
|
|
6
6
|
function checkBasicAuth(req, auth) {
|
|
7
7
|
const header = req.headers['authorization'] ?? '';
|
|
8
|
-
|
|
8
|
+
const [scheme, encoded] = header.split(' ');
|
|
9
|
+
if (scheme !== 'Basic' || !encoded)
|
|
9
10
|
return false;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
return user === auth.username && rest.join(':') === auth.password;
|
|
11
|
+
const [username, password] = Buffer.from(encoded, 'base64').toString('utf8').split(':');
|
|
12
|
+
return username === auth.username && password === auth.password;
|
|
13
13
|
}
|
|
14
14
|
function rejectUnauthorized(res) {
|
|
15
|
-
res.writeHead(401, {
|
|
16
|
-
'WWW-Authenticate': 'Basic realm="Scheduler Dashboard"',
|
|
17
|
-
'Content-Type': 'text/plain',
|
|
18
|
-
});
|
|
15
|
+
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Scheduler Dashboard"' });
|
|
19
16
|
res.end('Unauthorized');
|
|
20
17
|
}
|
|
21
18
|
function createAuthGuard(auth) {
|
|
19
|
+
if (!auth)
|
|
20
|
+
return (_req, _res, next) => next();
|
|
22
21
|
return (req, res, next) => {
|
|
23
|
-
if (!auth)
|
|
24
|
-
return
|
|
25
|
-
if (!checkBasicAuth(req, auth)) {
|
|
26
|
-
res.setHeader('WWW-Authenticate', 'Basic realm="Scheduler Dashboard"');
|
|
27
|
-
return res.status(401).send('Unauthorized');
|
|
28
|
-
}
|
|
22
|
+
if (!checkBasicAuth(req, auth))
|
|
23
|
+
return rejectUnauthorized(res);
|
|
29
24
|
next();
|
|
30
25
|
};
|
|
31
26
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { JobsService } from './jobs.service';
|
|
2
|
-
export declare class
|
|
2
|
+
export declare class DashboardController {
|
|
3
3
|
private readonly jobsService;
|
|
4
4
|
constructor(jobsService: JobsService);
|
|
5
|
-
dashboard(): string;
|
|
6
|
-
dashboardDetail(): string;
|
|
7
5
|
getJobs(): {
|
|
8
6
|
cron: {
|
|
9
7
|
name: string;
|
|
@@ -11,6 +9,7 @@ export declare class JobsController {
|
|
|
11
9
|
running: boolean;
|
|
12
10
|
nextRun: string;
|
|
13
11
|
history: import(".").JobExecution[];
|
|
12
|
+
metrics: import(".").JobMetrics;
|
|
14
13
|
}[];
|
|
15
14
|
intervals: {
|
|
16
15
|
name: string;
|
|
@@ -22,4 +21,7 @@ export declare class JobsController {
|
|
|
22
21
|
triggerJob(name: string): {
|
|
23
22
|
triggered: string;
|
|
24
23
|
};
|
|
24
|
+
stopExecution(id: string): {
|
|
25
|
+
stopped: string;
|
|
26
|
+
};
|
|
25
27
|
}
|
|
@@ -12,61 +12,51 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
15
|
+
exports.DashboardController = void 0;
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
|
-
const basic_auth_guard_1 = require("./basic-auth.guard");
|
|
18
17
|
const jobs_service_1 = require("./jobs.service");
|
|
19
|
-
|
|
20
|
-
let JobsController = class JobsController {
|
|
18
|
+
let DashboardController = class DashboardController {
|
|
21
19
|
constructor(jobsService) {
|
|
22
20
|
this.jobsService = jobsService;
|
|
23
21
|
}
|
|
24
|
-
dashboard() {
|
|
25
|
-
return dashboard_1.dashboardHtml;
|
|
26
|
-
}
|
|
27
|
-
dashboardDetail() {
|
|
28
|
-
return dashboard_1.dashboardHtml;
|
|
29
|
-
}
|
|
30
22
|
getJobs() {
|
|
31
23
|
return this.jobsService.getJobs();
|
|
32
24
|
}
|
|
33
25
|
triggerJob(name) {
|
|
34
26
|
const ok = this.jobsService.triggerJob(name);
|
|
35
27
|
if (!ok)
|
|
36
|
-
throw new common_1.
|
|
28
|
+
throw new common_1.HttpException(`Job "${name}" not found`, common_1.HttpStatus.NOT_FOUND);
|
|
37
29
|
return { triggered: name };
|
|
38
30
|
}
|
|
31
|
+
stopExecution(id) {
|
|
32
|
+
const ok = this.jobsService.stopExecution(id);
|
|
33
|
+
if (!ok)
|
|
34
|
+
throw new common_1.HttpException(`Execution "${id}" not found or already finished`, common_1.HttpStatus.NOT_FOUND);
|
|
35
|
+
return { stopped: id };
|
|
36
|
+
}
|
|
39
37
|
};
|
|
40
|
-
exports.
|
|
38
|
+
exports.DashboardController = DashboardController;
|
|
41
39
|
__decorate([
|
|
42
40
|
(0, common_1.Get)(),
|
|
43
|
-
(0, common_1.Header)('Content-Type', 'text/html; charset=utf-8'),
|
|
44
|
-
__metadata("design:type", Function),
|
|
45
|
-
__metadata("design:paramtypes", []),
|
|
46
|
-
__metadata("design:returntype", void 0)
|
|
47
|
-
], JobsController.prototype, "dashboard", null);
|
|
48
|
-
__decorate([
|
|
49
|
-
(0, common_1.Get)('jobs/:name'),
|
|
50
|
-
(0, common_1.Header)('Content-Type', 'text/html; charset=utf-8'),
|
|
51
41
|
__metadata("design:type", Function),
|
|
52
42
|
__metadata("design:paramtypes", []),
|
|
53
43
|
__metadata("design:returntype", void 0)
|
|
54
|
-
],
|
|
44
|
+
], DashboardController.prototype, "getJobs", null);
|
|
55
45
|
__decorate([
|
|
56
|
-
(0, common_1.
|
|
46
|
+
(0, common_1.Post)(':name/trigger'),
|
|
47
|
+
__param(0, (0, common_1.Param)('name')),
|
|
57
48
|
__metadata("design:type", Function),
|
|
58
|
-
__metadata("design:paramtypes", []),
|
|
49
|
+
__metadata("design:paramtypes", [String]),
|
|
59
50
|
__metadata("design:returntype", void 0)
|
|
60
|
-
],
|
|
51
|
+
], DashboardController.prototype, "triggerJob", null);
|
|
61
52
|
__decorate([
|
|
62
|
-
(0, common_1.Post)('
|
|
63
|
-
__param(0, (0, common_1.Param)('
|
|
53
|
+
(0, common_1.Post)('executions/:id/stop'),
|
|
54
|
+
__param(0, (0, common_1.Param)('id')),
|
|
64
55
|
__metadata("design:type", Function),
|
|
65
56
|
__metadata("design:paramtypes", [String]),
|
|
66
57
|
__metadata("design:returntype", void 0)
|
|
67
|
-
],
|
|
68
|
-
exports.
|
|
69
|
-
(0, common_1.Controller)(),
|
|
70
|
-
(0, common_1.UseGuards)(basic_auth_guard_1.BasicAuthGuard),
|
|
58
|
+
], DashboardController.prototype, "stopExecution", null);
|
|
59
|
+
exports.DashboardController = DashboardController = __decorate([
|
|
60
|
+
(0, common_1.Controller)('api'),
|
|
71
61
|
__metadata("design:paramtypes", [jobs_service_1.JobsService])
|
|
72
|
-
],
|
|
62
|
+
], DashboardController);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DynamicModule, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { Storage } from './storage/storage.abstract';
|
|
3
|
-
|
|
3
|
+
import { SchedulerDashOptions } from './scheduler-dash.options';
|
|
4
|
+
export declare class DashboardModule implements OnModuleInit {
|
|
4
5
|
private readonly storage;
|
|
5
6
|
private readonly options;
|
|
6
|
-
private readonly logger;
|
|
7
7
|
constructor(storage: Storage, options: SchedulerDashOptions);
|
|
8
8
|
onModuleInit(): void;
|
|
9
|
+
static forRoot(options?: SchedulerDashOptions): DynamicModule;
|
|
9
10
|
}
|
|
@@ -11,30 +11,40 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
|
+
var DashboardModule_1;
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
16
|
+
exports.DashboardModule = void 0;
|
|
16
17
|
const common_1 = require("@nestjs/common");
|
|
17
|
-
const scheduler_dash_options_1 = require("./scheduler-dash.options");
|
|
18
|
-
const scheduler_dash_context_1 = require("./scheduler-dash.context");
|
|
19
18
|
const storage_abstract_1 = require("./storage/storage.abstract");
|
|
20
|
-
|
|
19
|
+
const memory_storage_1 = require("./storage/memory.storage");
|
|
20
|
+
const scheduler_dash_context_1 = require("./scheduler-dash.context");
|
|
21
|
+
const STORAGE_TOKEN = Symbol('DASHBOARD_STORAGE');
|
|
22
|
+
const OPTIONS_TOKEN = Symbol('DASHBOARD_OPTIONS');
|
|
23
|
+
let DashboardModule = DashboardModule_1 = class DashboardModule {
|
|
21
24
|
constructor(storage, options) {
|
|
22
25
|
this.storage = storage;
|
|
23
26
|
this.options = options;
|
|
24
|
-
this.logger = new common_1.Logger('SchedulerDash');
|
|
25
27
|
}
|
|
26
28
|
onModuleInit() {
|
|
27
29
|
scheduler_dash_context_1.SchedulerDashContext.storage = this.storage;
|
|
28
|
-
scheduler_dash_context_1.SchedulerDashContext.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
scheduler_dash_context_1.SchedulerDashContext.noOverlap = this.options.noOverlap ?? false;
|
|
31
|
+
scheduler_dash_context_1.SchedulerDashContext.maxConcurrent = this.options.maxConcurrent;
|
|
32
|
+
}
|
|
33
|
+
static forRoot(options = {}) {
|
|
34
|
+
const storage = options.storage ?? new memory_storage_1.MemoryStorage({ historyRetention: 10 });
|
|
35
|
+
return {
|
|
36
|
+
module: DashboardModule_1,
|
|
37
|
+
providers: [
|
|
38
|
+
{ provide: STORAGE_TOKEN, useValue: storage },
|
|
39
|
+
{ provide: OPTIONS_TOKEN, useValue: options },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
32
42
|
}
|
|
33
43
|
};
|
|
34
|
-
exports.
|
|
35
|
-
exports.
|
|
36
|
-
(0, common_1.
|
|
37
|
-
__param(0, (0, common_1.Inject)(
|
|
38
|
-
__param(1, (0, common_1.Inject)(
|
|
44
|
+
exports.DashboardModule = DashboardModule;
|
|
45
|
+
exports.DashboardModule = DashboardModule = DashboardModule_1 = __decorate([
|
|
46
|
+
(0, common_1.Module)({}),
|
|
47
|
+
__param(0, (0, common_1.Inject)(STORAGE_TOKEN)),
|
|
48
|
+
__param(1, (0, common_1.Inject)(OPTIONS_TOKEN)),
|
|
39
49
|
__metadata("design:paramtypes", [storage_abstract_1.Storage, Object])
|
|
40
|
-
],
|
|
50
|
+
], DashboardModule);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { Storage } from '../storage/storage.abstract';
|
|
2
|
+
interface QueueEntry {
|
|
3
3
|
instance: unknown;
|
|
4
4
|
args: unknown[];
|
|
5
5
|
jobName: string;
|
|
@@ -10,13 +10,13 @@ export interface QueuedEntry {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function isOverlapping(jobName: string, noOverlap: boolean): boolean;
|
|
12
12
|
export declare function isConcurrencyLimitReached(): boolean;
|
|
13
|
-
declare function onJobStart(jobName: string): void;
|
|
14
|
-
declare function onJobEnd(jobName: string): void;
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
13
|
+
export declare function onJobStart(jobName: string): void;
|
|
14
|
+
export declare function onJobEnd(jobName: string): void;
|
|
15
|
+
export declare function enqueueEntry(entry: QueueEntry): void;
|
|
16
|
+
export declare function registerRunningExecution(id: string, jobName: string, storage: Storage): void;
|
|
17
|
+
export declare function unregisterRunningExecution(id: string): void;
|
|
18
|
+
export declare function wasExecutionStopped(id: string): boolean;
|
|
19
|
+
export declare function consumeStoppedExecution(id: string): boolean;
|
|
19
20
|
export declare function stopExecutionById(executionId: string): boolean;
|
|
20
|
-
export declare function
|
|
21
|
-
export
|
|
22
|
-
export { onJobStart, onJobEnd };
|
|
21
|
+
export declare function runEntry(entry: QueueEntry): void;
|
|
22
|
+
export {};
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isOverlapping = isOverlapping;
|
|
4
4
|
exports.isConcurrencyLimitReached = isConcurrencyLimitReached;
|
|
5
|
+
exports.onJobStart = onJobStart;
|
|
6
|
+
exports.onJobEnd = onJobEnd;
|
|
7
|
+
exports.enqueueEntry = enqueueEntry;
|
|
5
8
|
exports.registerRunningExecution = registerRunningExecution;
|
|
6
9
|
exports.unregisterRunningExecution = unregisterRunningExecution;
|
|
7
10
|
exports.wasExecutionStopped = wasExecutionStopped;
|
|
8
11
|
exports.consumeStoppedExecution = consumeStoppedExecution;
|
|
9
12
|
exports.stopExecutionById = stopExecutionById;
|
|
10
|
-
exports.enqueueEntry = enqueueEntry;
|
|
11
13
|
exports.runEntry = runEntry;
|
|
12
|
-
exports.onJobStart = onJobStart;
|
|
13
|
-
exports.onJobEnd = onJobEnd;
|
|
14
14
|
const scheduler_dash_context_1 = require("../scheduler-dash.context");
|
|
15
15
|
let runningCount = 0;
|
|
16
16
|
const runningJobs = new Set();
|
|
@@ -33,17 +33,20 @@ function onJobEnd(jobName) {
|
|
|
33
33
|
runningJobs.delete(jobName);
|
|
34
34
|
drainQueue();
|
|
35
35
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
36
|
+
function enqueueEntry(entry) {
|
|
37
|
+
queue.push(entry);
|
|
38
|
+
}
|
|
39
|
+
function registerRunningExecution(id, jobName, storage) {
|
|
40
|
+
runningExecutions.set(id, { jobName, storage });
|
|
38
41
|
}
|
|
39
|
-
function unregisterRunningExecution(
|
|
40
|
-
runningExecutions.delete(
|
|
42
|
+
function unregisterRunningExecution(id) {
|
|
43
|
+
runningExecutions.delete(id);
|
|
41
44
|
}
|
|
42
|
-
function wasExecutionStopped(
|
|
43
|
-
return stoppedExecutions.has(
|
|
45
|
+
function wasExecutionStopped(id) {
|
|
46
|
+
return stoppedExecutions.has(id);
|
|
44
47
|
}
|
|
45
|
-
function consumeStoppedExecution(
|
|
46
|
-
return stoppedExecutions.delete(
|
|
48
|
+
function consumeStoppedExecution(id) {
|
|
49
|
+
return stoppedExecutions.delete(id);
|
|
47
50
|
}
|
|
48
51
|
function stopExecutionById(executionId) {
|
|
49
52
|
const queueIdx = queue.findIndex(e => e.executionId === executionId);
|
|
@@ -62,47 +65,39 @@ function stopExecutionById(executionId) {
|
|
|
62
65
|
}
|
|
63
66
|
return false;
|
|
64
67
|
}
|
|
65
|
-
function enqueueEntry(entry) {
|
|
66
|
-
queue.push(entry);
|
|
67
|
-
}
|
|
68
|
-
function nextEligibleIndex() {
|
|
69
|
-
return queue.findIndex(e => !e.noOverlap || !runningJobs.has(e.jobName));
|
|
70
|
-
}
|
|
71
68
|
function drainQueue() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
break;
|
|
79
|
-
const [entry] = queue.splice(idx, 1);
|
|
69
|
+
while (queue.length > 0 && !isConcurrencyLimitReached()) {
|
|
70
|
+
const entry = queue.shift();
|
|
71
|
+
if (isOverlapping(entry.jobName, entry.noOverlap)) {
|
|
72
|
+
entry.storage.update(entry.executionId, { finishedAt: new Date(), status: 'stopped' });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
80
75
|
runEntry(entry);
|
|
81
76
|
}
|
|
82
77
|
}
|
|
83
|
-
function formatError(err) {
|
|
84
|
-
return err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
85
|
-
}
|
|
86
78
|
function runEntry(entry) {
|
|
87
79
|
const { instance, args, jobName, executionId, original, storage } = entry;
|
|
80
|
+
storage.update(executionId, { status: 'running' });
|
|
88
81
|
onJobStart(jobName);
|
|
89
82
|
registerRunningExecution(executionId, jobName, storage);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
(async () => {
|
|
84
|
+
try {
|
|
85
|
+
await original.apply(instance, args);
|
|
86
|
+
if (!wasExecutionStopped(executionId)) {
|
|
87
|
+
storage.update(executionId, { finishedAt: new Date(), status: 'completed' });
|
|
88
|
+
}
|
|
95
89
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!wasExecutionStopped(executionId)) {
|
|
92
|
+
const error = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
93
|
+
storage.update(executionId, { finishedAt: new Date(), status: 'failed', error });
|
|
94
|
+
}
|
|
100
95
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
finally {
|
|
97
|
+
unregisterRunningExecution(executionId);
|
|
98
|
+
if (!consumeStoppedExecution(executionId)) {
|
|
99
|
+
onJobEnd(jobName);
|
|
100
|
+
}
|
|
106
101
|
}
|
|
107
|
-
});
|
|
102
|
+
})();
|
|
108
103
|
}
|
|
@@ -8,9 +8,6 @@ const job_concurrency_1 = require("./job-concurrency");
|
|
|
8
8
|
function formatError(err) {
|
|
9
9
|
return err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
10
10
|
}
|
|
11
|
-
function resolveNoOverlap(jobNoOverlap) {
|
|
12
|
-
return jobNoOverlap ?? scheduler_dash_context_1.SchedulerDashContext.noOverlap;
|
|
13
|
-
}
|
|
14
11
|
async function runWithoutStorage(instance, args, jobName, original) {
|
|
15
12
|
(0, job_concurrency_1.onJobStart)(jobName);
|
|
16
13
|
try {
|
|
@@ -55,10 +52,10 @@ function queueExecution(instance, args, jobName, noOverlap, original) {
|
|
|
55
52
|
function TrackJob(cronTime, options) {
|
|
56
53
|
return (target, propertyKey, descriptor) => {
|
|
57
54
|
const original = descriptor.value;
|
|
58
|
-
const jobName = options?.name ?? String(propertyKey)
|
|
55
|
+
const jobName = options?.name ?? `${target.constructor.name}.${String(propertyKey)}`;
|
|
59
56
|
const jobNoOverlap = options?.noOverlap;
|
|
60
57
|
descriptor.value = async function (...args) {
|
|
61
|
-
const noOverlap =
|
|
58
|
+
const noOverlap = jobNoOverlap ?? scheduler_dash_context_1.SchedulerDashContext.noOverlap;
|
|
62
59
|
const storage = scheduler_dash_context_1.SchedulerDashContext.storage;
|
|
63
60
|
if ((0, job_concurrency_1.isOverlapping)(jobName, noOverlap))
|
|
64
61
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INestApplication } from '@nestjs/common';
|
|
1
|
+
import type { INestApplication } from '@nestjs/common';
|
|
2
2
|
import { SchedulerDashOptions } from './scheduler-dash.options';
|
|
3
3
|
export { TrackJob } from './decorators/track-job.decorator';
|
|
4
4
|
export { Storage } from './storage/storage.abstract';
|