@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.
Files changed (34) hide show
  1. package/README.md +44 -69
  2. package/dist/auth.d.ts +1 -1
  3. package/dist/auth.js +9 -14
  4. package/dist/{jobs.controller.d.ts → dashboard.controller.d.ts} +5 -3
  5. package/dist/{jobs.controller.js → dashboard.controller.js} +21 -31
  6. package/dist/{jobs-tracker.service.d.ts → dashboard.module.d.ts} +4 -3
  7. package/dist/{jobs-tracker.service.js → dashboard.module.js} +25 -15
  8. package/dist/decorators/job-concurrency.d.ts +11 -11
  9. package/dist/decorators/job-concurrency.js +38 -43
  10. package/dist/decorators/track-job.decorator.js +2 -5
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +22 -19
  13. package/dist/jobs.service.js +1 -1
  14. package/dist/scheduler-dash.context.d.ts +6 -7
  15. package/dist/scheduler-dash.context.js +15 -6
  16. package/dist/scheduler-dash.options.d.ts +0 -1
  17. package/dist/scheduler-dash.schema.d.ts +0 -1
  18. package/dist/scheduler-dash.schema.js +0 -4
  19. package/dist/standalone-server.d.ts +1 -2
  20. package/dist/standalone-server.js +32 -64
  21. package/dist/ui/dashboard.d.ts +1 -1
  22. package/dist/ui/dashboard.js +1 -1
  23. package/package.json +13 -13
  24. package/ui/assets/index-BerSFPJX.css +1 -0
  25. package/ui/assets/index-C5PR13H0.js +235 -0
  26. package/ui/index.html +21 -0
  27. package/dist/basic-auth.guard.d.ts +0 -7
  28. package/dist/basic-auth.guard.js +0 -44
  29. package/dist/dashboard.server.d.ts +0 -4
  30. package/dist/dashboard.server.js +0 -63
  31. package/dist/embedded-server.d.ts +0 -4
  32. package/dist/embedded-server.js +0 -47
  33. package/dist/scheduler-dash.module.d.ts +0 -5
  34. 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 a queue
14
- - No-overlap mode: skip or queue a job if it is already running
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
- - Standalone server mode (separate port) or embedded in your existing app
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. Initialize the dashboard in `main.ts`
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
- await setupSchedulerDash(app);
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 the following options:
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
- | `basePath` | `string` | `'_jobs'` | URL path where the dashboard is mounted |
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 all dashboard routes with HTTP Basic Auth |
93
+ | `auth` | `{ username, password }` | — | Protect the dashboard with HTTP Basic Auth |
91
94
 
92
95
  ### `storage`
93
96
 
94
- The storage backend persists execution history and metrics. The built-in `MemoryStorage` keeps everything in-process.
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 if old history entries are removed, the counters for total runs, failed runs, and average duration keep accumulating.
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 the total number of `@TrackJob` jobs that can execute simultaneously across your entire application. Jobs that cannot start immediately are saved to storage with status `"queued"` and run in FIFO order as slots free up.
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, // at most 5 jobs running at the same time
136
+ maxConcurrent: 5,
157
137
  });
158
138
  ```
159
139
 
160
140
  ### `noOverlap`
161
141
 
162
- Prevents a job from firing again if the same job is still running. Applies globally to all `@TrackJob`-decorated methods.
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 set per job via the decorator, which takes precedence over the global setting:
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: 'supersecret',
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
- `@TrackJob` is a drop-in replacement for `@Cron` from `@nestjs/schedule`. It accepts all the same options plus `noOverlap`.
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` | Human-readable job name shown in the dashboard. Defaults to the method name |
204
- | `options.noOverlap` | `boolean` | Skip this job if it is already running. Overrides the global `noOverlap` setting |
205
- | `options.*` | — | All other [`CronOptions`](https://docs.nestjs.com/techniques/task-scheduling) from `@nestjs/schedule` are passed through |
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
- ## Full configuration example
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): (req: any, res: any, next: any) => any;
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
- if (!header.startsWith('Basic '))
8
+ const [scheme, encoded] = header.split(' ');
9
+ if (scheme !== 'Basic' || !encoded)
9
10
  return false;
10
- const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
11
- const [user, ...rest] = decoded.split(':');
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 next();
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 JobsController {
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.JobsController = void 0;
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
- const dashboard_1 = require("./ui/dashboard");
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.NotFoundException(`Job "${name}" not found`);
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.JobsController = JobsController;
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
- ], JobsController.prototype, "dashboardDetail", null);
44
+ ], DashboardController.prototype, "getJobs", null);
55
45
  __decorate([
56
- (0, common_1.Get)('api'),
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
- ], JobsController.prototype, "getJobs", null);
51
+ ], DashboardController.prototype, "triggerJob", null);
61
52
  __decorate([
62
- (0, common_1.Post)('api/:name/trigger'),
63
- __param(0, (0, common_1.Param)('name')),
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
- ], JobsController.prototype, "triggerJob", null);
68
- exports.JobsController = JobsController = __decorate([
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
- ], JobsController);
62
+ ], DashboardController);
@@ -1,9 +1,10 @@
1
- import { SchedulerDashOptions } from './scheduler-dash.options';
1
+ import { DynamicModule, OnModuleInit } from '@nestjs/common';
2
2
  import { Storage } from './storage/storage.abstract';
3
- export declare class JobsTrackerService {
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.JobsTrackerService = void 0;
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
- let JobsTrackerService = class JobsTrackerService {
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.basePath = this.options.basePath ?? '_jobs';
29
- const basePath = scheduler_dash_context_1.SchedulerDashContext.basePath;
30
- this.logger.log(`Dashboard available at /${basePath}`);
31
- this.logger.log(`API available at /${basePath}/api`);
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.JobsTrackerService = JobsTrackerService;
35
- exports.JobsTrackerService = JobsTrackerService = __decorate([
36
- (0, common_1.Injectable)(),
37
- __param(0, (0, common_1.Inject)(scheduler_dash_options_1.SCHEDULER_DASH_STORAGE)),
38
- __param(1, (0, common_1.Inject)(scheduler_dash_options_1.SCHEDULER_DASH_OPTIONS)),
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
- ], JobsTrackerService);
50
+ ], DashboardModule);
@@ -1,5 +1,5 @@
1
- import type { Storage } from '../storage/storage.abstract';
2
- export interface QueuedEntry {
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 registerRunningExecution(executionId: string, jobName: string, storage: Storage): void;
16
- export declare function unregisterRunningExecution(executionId: string): void;
17
- export declare function wasExecutionStopped(executionId: string): boolean;
18
- export declare function consumeStoppedExecution(executionId: string): boolean;
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 enqueueEntry(entry: QueuedEntry): void;
21
- export declare function runEntry(entry: QueuedEntry): void;
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 registerRunningExecution(executionId, jobName, storage) {
37
- runningExecutions.set(executionId, { jobName, storage });
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(executionId) {
40
- runningExecutions.delete(executionId);
42
+ function unregisterRunningExecution(id) {
43
+ runningExecutions.delete(id);
41
44
  }
42
- function wasExecutionStopped(executionId) {
43
- return stoppedExecutions.has(executionId);
45
+ function wasExecutionStopped(id) {
46
+ return stoppedExecutions.has(id);
44
47
  }
45
- function consumeStoppedExecution(executionId) {
46
- return stoppedExecutions.delete(executionId);
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
- const max = scheduler_dash_context_1.SchedulerDashContext.maxConcurrent;
73
- if (max === undefined)
74
- return;
75
- while (runningCount < max) {
76
- const idx = nextEligibleIndex();
77
- if (idx === -1)
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
- storage.update(executionId, { status: 'running' });
91
- Promise.resolve(original.apply(instance, args))
92
- .then(() => {
93
- if (!wasExecutionStopped(executionId)) {
94
- storage.update(executionId, { finishedAt: new Date(), status: 'completed' });
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
- .catch((err) => {
98
- if (!wasExecutionStopped(executionId)) {
99
- storage.update(executionId, { finishedAt: new Date(), status: 'failed', error: formatError(err) });
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
- .finally(() => {
103
- unregisterRunningExecution(executionId);
104
- if (!consumeStoppedExecution(executionId)) {
105
- onJobEnd(jobName);
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 = resolveNoOverlap(jobNoOverlap);
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';