@objectstack/service-job 4.0.4 → 4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -177
- package/src/cron-job-adapter.ts +0 -51
- package/src/index.ts +0 -8
- package/src/interval-job-adapter.test.ts +0 -120
- package/src/interval-job-adapter.ts +0 -130
- package/src/job-service-plugin.ts +0 -65
- package/tsconfig.json +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-job",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling",
|
|
6
6
|
"type": "module",
|
|
@@ -14,13 +14,38 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@objectstack/core": "4.0.
|
|
18
|
-
"@objectstack/spec": "4.0.
|
|
17
|
+
"@objectstack/core": "4.0.5",
|
|
18
|
+
"@objectstack/spec": "4.0.5"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@types/node": "^25.6.
|
|
22
|
-
"typescript": "^6.0.
|
|
23
|
-
"vitest": "^4.1.
|
|
21
|
+
"@types/node": "^25.6.2",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"vitest": "^4.1.5"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"service",
|
|
28
|
+
"job",
|
|
29
|
+
"cron",
|
|
30
|
+
"scheduler"
|
|
31
|
+
],
|
|
32
|
+
"author": "ObjectStack",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/objectstack-ai/framework.git",
|
|
36
|
+
"directory": "packages/services/service-job"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://objectstack.ai/docs",
|
|
39
|
+
"bugs": "https://github.com/objectstack-ai/framework/issues",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
24
49
|
},
|
|
25
50
|
"scripts": {
|
|
26
51
|
"build": "tsup --config ../../../tsup.config.ts",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @objectstack/service-job@4.0.4 build /home/runner/work/framework/framework/packages/services/service-job
|
|
3
|
-
> tsup --config ../../../tsup.config.ts
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: es2020
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.js [22m[32m3.99 KB[39m
|
|
14
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m10.88 KB[39m
|
|
15
|
-
[32mESM[39m ⚡️ Build success in 82ms
|
|
16
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m5.09 KB[39m
|
|
17
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[32m11.42 KB[39m
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in 84ms
|
|
19
|
-
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in 15151ms
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.60 KB[39m
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[32m3.60 KB[39m
|
package/CHANGELOG.md
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# @objectstack/service-job
|
|
2
|
-
|
|
3
|
-
## 4.0.4
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [326b66b]
|
|
8
|
-
- @objectstack/spec@4.0.4
|
|
9
|
-
- @objectstack/core@4.0.4
|
|
10
|
-
|
|
11
|
-
## 4.0.3
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- @objectstack/spec@4.0.3
|
|
16
|
-
- @objectstack/core@4.0.3
|
|
17
|
-
|
|
18
|
-
## 4.0.2
|
|
19
|
-
|
|
20
|
-
### Patch Changes
|
|
21
|
-
|
|
22
|
-
- Updated dependencies [5f659e9]
|
|
23
|
-
- @objectstack/spec@4.0.2
|
|
24
|
-
- @objectstack/core@4.0.2
|
|
25
|
-
|
|
26
|
-
## 4.0.0
|
|
27
|
-
|
|
28
|
-
### Patch Changes
|
|
29
|
-
|
|
30
|
-
- Updated dependencies [f08ffc3]
|
|
31
|
-
- Updated dependencies [e0b0a78]
|
|
32
|
-
- @objectstack/spec@4.0.0
|
|
33
|
-
- @objectstack/core@4.0.0
|
|
34
|
-
|
|
35
|
-
## 3.3.1
|
|
36
|
-
|
|
37
|
-
### Patch Changes
|
|
38
|
-
|
|
39
|
-
- @objectstack/spec@3.3.1
|
|
40
|
-
- @objectstack/core@3.3.1
|
|
41
|
-
|
|
42
|
-
## 3.3.0
|
|
43
|
-
|
|
44
|
-
### Patch Changes
|
|
45
|
-
|
|
46
|
-
- @objectstack/spec@3.3.0
|
|
47
|
-
- @objectstack/core@3.3.0
|
|
48
|
-
|
|
49
|
-
## 3.2.9
|
|
50
|
-
|
|
51
|
-
### Patch Changes
|
|
52
|
-
|
|
53
|
-
- @objectstack/spec@3.2.9
|
|
54
|
-
- @objectstack/core@3.2.9
|
|
55
|
-
|
|
56
|
-
## 3.2.8
|
|
57
|
-
|
|
58
|
-
### Patch Changes
|
|
59
|
-
|
|
60
|
-
- @objectstack/spec@3.2.8
|
|
61
|
-
- @objectstack/core@3.2.8
|
|
62
|
-
|
|
63
|
-
## 3.2.7
|
|
64
|
-
|
|
65
|
-
### Patch Changes
|
|
66
|
-
|
|
67
|
-
- @objectstack/spec@3.2.7
|
|
68
|
-
- @objectstack/core@3.2.7
|
|
69
|
-
|
|
70
|
-
## 3.2.6
|
|
71
|
-
|
|
72
|
-
### Patch Changes
|
|
73
|
-
|
|
74
|
-
- @objectstack/spec@3.2.6
|
|
75
|
-
- @objectstack/core@3.2.6
|
|
76
|
-
|
|
77
|
-
## 3.2.5
|
|
78
|
-
|
|
79
|
-
### Patch Changes
|
|
80
|
-
|
|
81
|
-
- @objectstack/spec@3.2.5
|
|
82
|
-
- @objectstack/core@3.2.5
|
|
83
|
-
|
|
84
|
-
## 3.2.4
|
|
85
|
-
|
|
86
|
-
### Patch Changes
|
|
87
|
-
|
|
88
|
-
- @objectstack/spec@3.2.4
|
|
89
|
-
- @objectstack/core@3.2.4
|
|
90
|
-
|
|
91
|
-
## 3.2.3
|
|
92
|
-
|
|
93
|
-
### Patch Changes
|
|
94
|
-
|
|
95
|
-
- @objectstack/spec@3.2.3
|
|
96
|
-
- @objectstack/core@3.2.3
|
|
97
|
-
|
|
98
|
-
## 3.2.2
|
|
99
|
-
|
|
100
|
-
### Patch Changes
|
|
101
|
-
|
|
102
|
-
- Updated dependencies [46defbb]
|
|
103
|
-
- @objectstack/spec@3.2.2
|
|
104
|
-
- @objectstack/core@3.2.2
|
|
105
|
-
|
|
106
|
-
## 3.2.1
|
|
107
|
-
|
|
108
|
-
### Patch Changes
|
|
109
|
-
|
|
110
|
-
- Updated dependencies [850b546]
|
|
111
|
-
- @objectstack/spec@3.2.1
|
|
112
|
-
- @objectstack/core@3.2.1
|
|
113
|
-
|
|
114
|
-
## 3.2.0
|
|
115
|
-
|
|
116
|
-
### Patch Changes
|
|
117
|
-
|
|
118
|
-
- Updated dependencies [5901c29]
|
|
119
|
-
- @objectstack/spec@3.2.0
|
|
120
|
-
- @objectstack/core@3.2.0
|
|
121
|
-
|
|
122
|
-
## 3.1.1
|
|
123
|
-
|
|
124
|
-
### Patch Changes
|
|
125
|
-
|
|
126
|
-
- Updated dependencies [953d667]
|
|
127
|
-
- @objectstack/spec@3.1.1
|
|
128
|
-
- @objectstack/core@3.1.1
|
|
129
|
-
|
|
130
|
-
## 3.1.0
|
|
131
|
-
|
|
132
|
-
### Patch Changes
|
|
133
|
-
|
|
134
|
-
- Updated dependencies [0088830]
|
|
135
|
-
- @objectstack/spec@3.1.0
|
|
136
|
-
- @objectstack/core@3.1.0
|
|
137
|
-
|
|
138
|
-
## 3.0.11
|
|
139
|
-
|
|
140
|
-
### Patch Changes
|
|
141
|
-
|
|
142
|
-
- Updated dependencies [92d9d99]
|
|
143
|
-
- @objectstack/spec@3.0.11
|
|
144
|
-
- @objectstack/core@3.0.11
|
|
145
|
-
|
|
146
|
-
## 3.0.10
|
|
147
|
-
|
|
148
|
-
### Patch Changes
|
|
149
|
-
|
|
150
|
-
- Updated dependencies [d1e5d31]
|
|
151
|
-
- @objectstack/spec@3.0.10
|
|
152
|
-
- @objectstack/core@3.0.10
|
|
153
|
-
|
|
154
|
-
## 3.0.9
|
|
155
|
-
|
|
156
|
-
### Patch Changes
|
|
157
|
-
|
|
158
|
-
- Updated dependencies [15e0df6]
|
|
159
|
-
- @objectstack/spec@3.0.9
|
|
160
|
-
- @objectstack/core@3.0.9
|
|
161
|
-
|
|
162
|
-
## 3.0.8
|
|
163
|
-
|
|
164
|
-
### Patch Changes
|
|
165
|
-
|
|
166
|
-
- Updated dependencies [5a968a2]
|
|
167
|
-
- @objectstack/spec@3.0.8
|
|
168
|
-
- @objectstack/core@3.0.8
|
|
169
|
-
|
|
170
|
-
## 3.0.7
|
|
171
|
-
|
|
172
|
-
### Patch Changes
|
|
173
|
-
|
|
174
|
-
- Updated dependencies [0119bd7]
|
|
175
|
-
- Updated dependencies [5426bdf]
|
|
176
|
-
- @objectstack/spec@3.0.7
|
|
177
|
-
- @objectstack/core@3.0.7
|
package/src/cron-job-adapter.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { IJobService, JobSchedule, JobHandler, JobExecution } from '@objectstack/spec/contracts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Configuration for the cron-based job adapter.
|
|
7
|
-
*/
|
|
8
|
-
export interface CronJobAdapterOptions {
|
|
9
|
-
/** Timezone for cron expressions (default: 'UTC') */
|
|
10
|
-
timezone?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Cron-based job adapter skeleton implementing IJobService.
|
|
15
|
-
*
|
|
16
|
-
* This is a placeholder for future cron integration (e.g., `node-cron` or `croner`).
|
|
17
|
-
* Concrete implementation will parse cron expressions and schedule jobs accordingly.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* const scheduler = new CronJobAdapter({ timezone: 'America/New_York' });
|
|
22
|
-
* await scheduler.schedule('nightly-cleanup', { type: 'cron', expression: '0 0 * * *' }, handler);
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export class CronJobAdapter implements IJobService {
|
|
26
|
-
private readonly timezone: string;
|
|
27
|
-
|
|
28
|
-
constructor(options: CronJobAdapterOptions = {}) {
|
|
29
|
-
this.timezone = options.timezone ?? 'UTC';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async schedule(_name: string, _schedule: JobSchedule, _handler: JobHandler): Promise<void> {
|
|
33
|
-
throw new Error(`CronJobAdapter not yet implemented (timezone: ${this.timezone})`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async cancel(_name: string): Promise<void> {
|
|
37
|
-
throw new Error('CronJobAdapter not yet implemented');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async trigger(_name: string, _data?: unknown): Promise<void> {
|
|
41
|
-
throw new Error('CronJobAdapter not yet implemented');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async getExecutions(_name: string, _limit?: number): Promise<JobExecution[]> {
|
|
45
|
-
throw new Error('CronJobAdapter not yet implemented');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async listJobs(): Promise<string[]> {
|
|
49
|
-
throw new Error('CronJobAdapter not yet implemented');
|
|
50
|
-
}
|
|
51
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
export { JobServicePlugin } from './job-service-plugin.js';
|
|
4
|
-
export type { JobServicePluginOptions } from './job-service-plugin.js';
|
|
5
|
-
export { IntervalJobAdapter } from './interval-job-adapter.js';
|
|
6
|
-
export type { IntervalJobAdapterOptions } from './interval-job-adapter.js';
|
|
7
|
-
export { CronJobAdapter } from './cron-job-adapter.js';
|
|
8
|
-
export type { CronJobAdapterOptions } from './cron-job-adapter.js';
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
4
|
-
import { IntervalJobAdapter } from './interval-job-adapter';
|
|
5
|
-
import type { IJobService } from '@objectstack/spec/contracts';
|
|
6
|
-
|
|
7
|
-
describe('IntervalJobAdapter', () => {
|
|
8
|
-
let adapter: IntervalJobAdapter;
|
|
9
|
-
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
await adapter?.destroy();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should implement IJobService contract', () => {
|
|
15
|
-
adapter = new IntervalJobAdapter();
|
|
16
|
-
const job: IJobService = adapter;
|
|
17
|
-
expect(typeof job.schedule).toBe('function');
|
|
18
|
-
expect(typeof job.cancel).toBe('function');
|
|
19
|
-
expect(typeof job.trigger).toBe('function');
|
|
20
|
-
expect(typeof job.getExecutions).toBe('function');
|
|
21
|
-
expect(typeof job.listJobs).toBe('function');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should schedule and list jobs', async () => {
|
|
25
|
-
adapter = new IntervalJobAdapter();
|
|
26
|
-
await adapter.schedule('daily-report', { type: 'cron', expression: '0 0 * * *' }, async () => {});
|
|
27
|
-
expect(await adapter.listJobs()).toEqual(['daily-report']);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should cancel a job', async () => {
|
|
31
|
-
adapter = new IntervalJobAdapter();
|
|
32
|
-
await adapter.schedule('temp-job', { type: 'cron', expression: '* * * * *' }, async () => {});
|
|
33
|
-
await adapter.cancel('temp-job');
|
|
34
|
-
expect(await adapter.listJobs()).toEqual([]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should trigger a job handler with data', async () => {
|
|
38
|
-
adapter = new IntervalJobAdapter();
|
|
39
|
-
let triggered = false;
|
|
40
|
-
let receivedCtx: any;
|
|
41
|
-
|
|
42
|
-
await adapter.schedule('my-job', { type: 'cron', expression: '* * * * *' }, async (ctx) => {
|
|
43
|
-
triggered = true;
|
|
44
|
-
receivedCtx = ctx;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
await adapter.trigger('my-job', { key: 'val' });
|
|
48
|
-
expect(triggered).toBe(true);
|
|
49
|
-
expect(receivedCtx.jobId).toBe('my-job');
|
|
50
|
-
expect(receivedCtx.data).toEqual({ key: 'val' });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should throw when triggering non-existent job', async () => {
|
|
54
|
-
adapter = new IntervalJobAdapter();
|
|
55
|
-
await expect(adapter.trigger('missing')).rejects.toThrow('Job "missing" not found');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should record execution history', async () => {
|
|
59
|
-
adapter = new IntervalJobAdapter();
|
|
60
|
-
await adapter.schedule('tracked-job', { type: 'cron', expression: '* * * * *' }, async () => {});
|
|
61
|
-
await adapter.trigger('tracked-job');
|
|
62
|
-
|
|
63
|
-
const execs = await adapter.getExecutions('tracked-job');
|
|
64
|
-
expect(execs).toHaveLength(1);
|
|
65
|
-
expect(execs[0].status).toBe('success');
|
|
66
|
-
expect(execs[0].jobId).toBe('tracked-job');
|
|
67
|
-
expect(execs[0].startedAt).toBeTruthy();
|
|
68
|
-
expect(execs[0].completedAt).toBeTruthy();
|
|
69
|
-
expect(typeof execs[0].durationMs).toBe('number');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should record failed executions', async () => {
|
|
73
|
-
adapter = new IntervalJobAdapter();
|
|
74
|
-
await adapter.schedule('fail-job', { type: 'cron', expression: '* * * * *' }, async () => {
|
|
75
|
-
throw new Error('Job failed');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await adapter.trigger('fail-job');
|
|
79
|
-
|
|
80
|
-
const execs = await adapter.getExecutions('fail-job');
|
|
81
|
-
expect(execs).toHaveLength(1);
|
|
82
|
-
expect(execs[0].status).toBe('failed');
|
|
83
|
-
expect(execs[0].error).toBe('Job failed');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should return empty executions for non-existent job', async () => {
|
|
87
|
-
adapter = new IntervalJobAdapter();
|
|
88
|
-
expect(await adapter.getExecutions('missing')).toEqual([]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should limit execution history with limit param', async () => {
|
|
92
|
-
adapter = new IntervalJobAdapter();
|
|
93
|
-
await adapter.schedule('multi-job', { type: 'cron', expression: '* * * * *' }, async () => {});
|
|
94
|
-
await adapter.trigger('multi-job');
|
|
95
|
-
await adapter.trigger('multi-job');
|
|
96
|
-
await adapter.trigger('multi-job');
|
|
97
|
-
|
|
98
|
-
const execs = await adapter.getExecutions('multi-job', 2);
|
|
99
|
-
expect(execs).toHaveLength(2);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should replace existing job with same name', async () => {
|
|
103
|
-
adapter = new IntervalJobAdapter();
|
|
104
|
-
let count = 0;
|
|
105
|
-
await adapter.schedule('dup', { type: 'cron', expression: '* * * * *' }, async () => { count = 1; });
|
|
106
|
-
await adapter.schedule('dup', { type: 'cron', expression: '* * * * *' }, async () => { count = 2; });
|
|
107
|
-
|
|
108
|
-
await adapter.trigger('dup');
|
|
109
|
-
expect(count).toBe(2);
|
|
110
|
-
expect(await adapter.listJobs()).toEqual(['dup']);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should clean up all timers on destroy', async () => {
|
|
114
|
-
adapter = new IntervalJobAdapter();
|
|
115
|
-
await adapter.schedule('j1', { type: 'interval', intervalMs: 100000 }, async () => {});
|
|
116
|
-
await adapter.schedule('j2', { type: 'interval', intervalMs: 100000 }, async () => {});
|
|
117
|
-
await adapter.destroy();
|
|
118
|
-
expect(await adapter.listJobs()).toEqual([]);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { IJobService, JobSchedule, JobHandler, JobExecution } from '@objectstack/spec/contracts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Internal record for a scheduled job.
|
|
7
|
-
*/
|
|
8
|
-
interface JobRecord {
|
|
9
|
-
name: string;
|
|
10
|
-
schedule: JobSchedule;
|
|
11
|
-
handler: JobHandler;
|
|
12
|
-
timerId?: ReturnType<typeof setInterval> | ReturnType<typeof setTimeout>;
|
|
13
|
-
executions: JobExecution[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Configuration options for IntervalJobAdapter.
|
|
18
|
-
*/
|
|
19
|
-
export interface IntervalJobAdapterOptions {
|
|
20
|
-
/** Maximum number of execution records to retain per job (default: 100) */
|
|
21
|
-
maxExecutions?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* setInterval-based job adapter implementing IJobService.
|
|
26
|
-
*
|
|
27
|
-
* Supports `interval` and `once` schedule types using Node.js timers.
|
|
28
|
-
* `cron` schedules are stored but not actively executed (requires a cron
|
|
29
|
-
* library — see CronJobAdapter skeleton).
|
|
30
|
-
*
|
|
31
|
-
* Suitable for single-process environments, development, and testing.
|
|
32
|
-
*/
|
|
33
|
-
export class IntervalJobAdapter implements IJobService {
|
|
34
|
-
private readonly jobs = new Map<string, JobRecord>();
|
|
35
|
-
private readonly maxExecutions: number;
|
|
36
|
-
|
|
37
|
-
constructor(options: IntervalJobAdapterOptions = {}) {
|
|
38
|
-
this.maxExecutions = options.maxExecutions ?? 100;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void> {
|
|
42
|
-
// Cancel any existing job with the same name
|
|
43
|
-
await this.cancel(name);
|
|
44
|
-
|
|
45
|
-
const record: JobRecord = { name, schedule, handler, executions: [] };
|
|
46
|
-
|
|
47
|
-
if (schedule.type === 'interval' && schedule.intervalMs) {
|
|
48
|
-
record.timerId = setInterval(async () => {
|
|
49
|
-
await this.executeJob(record);
|
|
50
|
-
}, schedule.intervalMs);
|
|
51
|
-
} else if (schedule.type === 'once' && schedule.at) {
|
|
52
|
-
const delay = new Date(schedule.at).getTime() - Date.now();
|
|
53
|
-
if (delay > 0) {
|
|
54
|
-
record.timerId = setTimeout(async () => {
|
|
55
|
-
await this.executeJob(record);
|
|
56
|
-
}, delay);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// 'cron' type: stored but not actively scheduled (needs cron library)
|
|
60
|
-
|
|
61
|
-
this.jobs.set(name, record);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async cancel(name: string): Promise<void> {
|
|
65
|
-
const record = this.jobs.get(name);
|
|
66
|
-
if (record?.timerId) {
|
|
67
|
-
clearInterval(record.timerId as ReturnType<typeof setInterval>);
|
|
68
|
-
clearTimeout(record.timerId as ReturnType<typeof setTimeout>);
|
|
69
|
-
}
|
|
70
|
-
this.jobs.delete(name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async trigger(name: string, data?: unknown): Promise<void> {
|
|
74
|
-
const record = this.jobs.get(name);
|
|
75
|
-
if (!record) {
|
|
76
|
-
throw new Error(`Job "${name}" not found`);
|
|
77
|
-
}
|
|
78
|
-
await this.executeJob(record, data);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async getExecutions(name: string, limit?: number): Promise<JobExecution[]> {
|
|
82
|
-
const record = this.jobs.get(name);
|
|
83
|
-
if (!record) return [];
|
|
84
|
-
const execs = record.executions;
|
|
85
|
-
return limit ? execs.slice(-limit) : execs;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async listJobs(): Promise<string[]> {
|
|
89
|
-
return [...this.jobs.keys()];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Stop all active timers. Call during plugin destroy phase.
|
|
94
|
-
*/
|
|
95
|
-
async destroy(): Promise<void> {
|
|
96
|
-
for (const record of this.jobs.values()) {
|
|
97
|
-
if (record.timerId) {
|
|
98
|
-
clearInterval(record.timerId as ReturnType<typeof setInterval>);
|
|
99
|
-
clearTimeout(record.timerId as ReturnType<typeof setTimeout>);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
this.jobs.clear();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private async executeJob(record: JobRecord, data?: unknown): Promise<void> {
|
|
106
|
-
const execution: JobExecution = {
|
|
107
|
-
jobId: record.name,
|
|
108
|
-
status: 'running',
|
|
109
|
-
startedAt: new Date().toISOString(),
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const startMs = Date.now();
|
|
113
|
-
try {
|
|
114
|
-
await record.handler({ jobId: record.name, data });
|
|
115
|
-
execution.status = 'success';
|
|
116
|
-
} catch (err) {
|
|
117
|
-
execution.status = 'failed';
|
|
118
|
-
execution.error = err instanceof Error ? err.message : String(err);
|
|
119
|
-
} finally {
|
|
120
|
-
execution.completedAt = new Date().toISOString();
|
|
121
|
-
execution.durationMs = Date.now() - startMs;
|
|
122
|
-
|
|
123
|
-
record.executions.push(execution);
|
|
124
|
-
// Trim old executions
|
|
125
|
-
if (record.executions.length > this.maxExecutions) {
|
|
126
|
-
record.executions.splice(0, record.executions.length - this.maxExecutions);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
-
import { IntervalJobAdapter } from './interval-job-adapter.js';
|
|
5
|
-
import type { IntervalJobAdapterOptions } from './interval-job-adapter.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Configuration options for the JobServicePlugin.
|
|
9
|
-
*/
|
|
10
|
-
export interface JobServicePluginOptions {
|
|
11
|
-
/** Job adapter type (default: 'interval') */
|
|
12
|
-
adapter?: 'interval' | 'cron';
|
|
13
|
-
/** Options for the interval job adapter */
|
|
14
|
-
interval?: IntervalJobAdapterOptions;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* JobServicePlugin — Production IJobService implementation.
|
|
19
|
-
*
|
|
20
|
-
* Registers a job scheduler with the kernel during the init phase.
|
|
21
|
-
* Supports setInterval-based and cron-based adapters.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* import { ObjectKernel } from '@objectstack/core';
|
|
26
|
-
* import { JobServicePlugin } from '@objectstack/service-job';
|
|
27
|
-
*
|
|
28
|
-
* const kernel = new ObjectKernel();
|
|
29
|
-
* kernel.use(new JobServicePlugin({ adapter: 'interval' }));
|
|
30
|
-
* await kernel.bootstrap();
|
|
31
|
-
*
|
|
32
|
-
* const job = kernel.getService('job');
|
|
33
|
-
* await job.schedule('cleanup', { type: 'interval', intervalMs: 60000 }, handler);
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export class JobServicePlugin implements Plugin {
|
|
37
|
-
name = 'com.objectstack.service.job';
|
|
38
|
-
version = '1.0.0';
|
|
39
|
-
type = 'standard';
|
|
40
|
-
|
|
41
|
-
private readonly options: JobServicePluginOptions;
|
|
42
|
-
private adapter?: IntervalJobAdapter;
|
|
43
|
-
|
|
44
|
-
constructor(options: JobServicePluginOptions = {}) {
|
|
45
|
-
this.options = { adapter: 'interval', ...options };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async init(ctx: PluginContext): Promise<void> {
|
|
49
|
-
const adapterType = this.options.adapter;
|
|
50
|
-
if (adapterType === 'cron') {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'Cron job adapter is not yet implemented. ' +
|
|
53
|
-
'Use adapter: "interval" or provide a custom IJobService via ctx.registerService("job", impl).'
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
this.adapter = new IntervalJobAdapter(this.options.interval);
|
|
58
|
-
ctx.registerService('job', this.adapter);
|
|
59
|
-
ctx.logger.info('JobServicePlugin: registered interval job adapter');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async destroy(): Promise<void> {
|
|
63
|
-
await this.adapter?.destroy();
|
|
64
|
-
}
|
|
65
|
-
}
|