@objectstack/service-job 4.0.3 → 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/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # @objectstack/service-job
2
+
3
+ Job Service for ObjectStack — implements `IJobService` with setInterval and cron scheduling.
4
+
5
+ ## Features
6
+
7
+ - **Cron Scheduling**: Schedule jobs with cron expressions
8
+ - **Interval Scheduling**: Run jobs at fixed intervals
9
+ - **Job Queue**: Manage job execution queue
10
+ - **Retry Logic**: Automatic retry on failure with exponential backoff
11
+ - **Job History**: Track execution history and status
12
+ - **Concurrency Control**: Limit concurrent job execution
13
+ - **Timezone Support**: Schedule jobs in specific timezones
14
+ - **Type-Safe**: Full TypeScript support
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @objectstack/service-job
20
+ ```
21
+
22
+ ## Basic Usage
23
+
24
+ ```typescript
25
+ import { defineStack } from '@objectstack/spec';
26
+ import { ServiceJob } from '@objectstack/service-job';
27
+
28
+ const stack = defineStack({
29
+ services: [
30
+ ServiceJob.configure({
31
+ timezone: 'America/New_York',
32
+ maxConcurrent: 5,
33
+ }),
34
+ ],
35
+ });
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ```typescript
41
+ interface JobServiceConfig {
42
+ /** Default timezone for cron jobs (default: 'UTC') */
43
+ timezone?: string;
44
+
45
+ /** Maximum concurrent job executions (default: 10) */
46
+ maxConcurrent?: number;
47
+
48
+ /** Enable job history tracking (default: true) */
49
+ enableHistory?: boolean;
50
+
51
+ /** Maximum history entries per job (default: 100) */
52
+ maxHistorySize?: number;
53
+ }
54
+ ```
55
+
56
+ ## Service API
57
+
58
+ ```typescript
59
+ // Get job service
60
+ const jobs = kernel.getService<IJobService>('job');
61
+ ```
62
+
63
+ ### Cron Jobs
64
+
65
+ ```typescript
66
+ // Schedule a job with cron expression
67
+ const job = await jobs.schedule({
68
+ name: 'daily_report',
69
+ schedule: '0 9 * * *', // Every day at 9 AM
70
+ handler: async (context) => {
71
+ console.log('Generating daily report...');
72
+ // Your job logic here
73
+ },
74
+ timezone: 'America/New_York',
75
+ });
76
+
77
+ // Common cron patterns:
78
+ // '*/5 * * * *' - Every 5 minutes
79
+ // '0 */2 * * *' - Every 2 hours
80
+ // '0 9 * * 1-5' - Weekdays at 9 AM
81
+ // '0 0 1 * *' - First day of every month at midnight
82
+ // '0 0 * * 0' - Every Sunday at midnight
83
+ ```
84
+
85
+ ### Interval Jobs
86
+
87
+ ```typescript
88
+ // Run every 30 seconds
89
+ const job = await jobs.scheduleInterval({
90
+ name: 'health_check',
91
+ interval: 30000, // milliseconds
92
+ handler: async (context) => {
93
+ console.log('Running health check...');
94
+ },
95
+ });
96
+
97
+ // Run every 5 minutes
98
+ const job = await jobs.scheduleInterval({
99
+ name: 'sync_data',
100
+ interval: 5 * 60 * 1000, // 5 minutes
101
+ handler: async (context) => {
102
+ // Sync data
103
+ },
104
+ });
105
+ ```
106
+
107
+ ### One-Time Jobs
108
+
109
+ ```typescript
110
+ // Schedule a one-time job
111
+ const job = await jobs.scheduleOnce({
112
+ name: 'send_reminder',
113
+ runAt: new Date('2024-12-25T09:00:00Z'),
114
+ handler: async (context) => {
115
+ console.log('Sending holiday reminder...');
116
+ },
117
+ });
118
+
119
+ // Schedule to run after a delay
120
+ const job = await jobs.scheduleOnce({
121
+ name: 'delayed_task',
122
+ delay: 3600000, // 1 hour from now
123
+ handler: async (context) => {
124
+ console.log('Executing delayed task...');
125
+ },
126
+ });
127
+ ```
128
+
129
+ ### Job Management
130
+
131
+ ```typescript
132
+ // List all jobs
133
+ const allJobs = await jobs.listJobs();
134
+
135
+ // Get job details
136
+ const job = await jobs.getJob('daily_report');
137
+
138
+ // Stop a job
139
+ await jobs.stopJob('daily_report');
140
+
141
+ // Resume a stopped job
142
+ await jobs.resumeJob('daily_report');
143
+
144
+ // Delete a job
145
+ await jobs.deleteJob('daily_report');
146
+
147
+ // Run a job immediately (ignoring schedule)
148
+ await jobs.runNow('daily_report');
149
+ ```
150
+
151
+ ## Advanced Features
152
+
153
+ ### Job Context
154
+
155
+ ```typescript
156
+ const job = await jobs.schedule({
157
+ name: 'process_orders',
158
+ schedule: '*/10 * * * *',
159
+ handler: async (context) => {
160
+ console.log('Job name:', context.jobName);
161
+ console.log('Execution ID:', context.executionId);
162
+ console.log('Scheduled time:', context.scheduledTime);
163
+ console.log('Execution count:', context.executionCount);
164
+
165
+ // Access services
166
+ const db = context.kernel.getService('database');
167
+ const orders = await db.find({ object: 'order', status: 'pending' });
168
+
169
+ // Process orders...
170
+ },
171
+ });
172
+ ```
173
+
174
+ ### Retry Configuration
175
+
176
+ ```typescript
177
+ const job = await jobs.schedule({
178
+ name: 'api_sync',
179
+ schedule: '0 * * * *', // Every hour
180
+ retry: {
181
+ maxAttempts: 3,
182
+ backoff: 'exponential', // 'linear' or 'exponential'
183
+ initialDelay: 1000, // 1 second
184
+ maxDelay: 60000, // 1 minute
185
+ },
186
+ handler: async (context) => {
187
+ // May fail and retry
188
+ await syncWithExternalAPI();
189
+ },
190
+ });
191
+ ```
192
+
193
+ ### Concurrency Control
194
+
195
+ ```typescript
196
+ const job = await jobs.schedule({
197
+ name: 'heavy_processing',
198
+ schedule: '*/5 * * * *',
199
+ concurrency: 1, // Only one instance can run at a time
200
+ handler: async (context) => {
201
+ // Long-running process
202
+ },
203
+ });
204
+ ```
205
+
206
+ ### Job History
207
+
208
+ ```typescript
209
+ // Get execution history for a job
210
+ const history = await jobs.getJobHistory('daily_report', {
211
+ limit: 50,
212
+ status: 'success', // 'success', 'failed', 'running'
213
+ });
214
+
215
+ // Example history entry:
216
+ // {
217
+ // executionId: 'exec:abc123',
218
+ // jobName: 'daily_report',
219
+ // status: 'success',
220
+ // startedAt: '2024-01-15T09:00:00Z',
221
+ // completedAt: '2024-01-15T09:05:23Z',
222
+ // duration: 323000, // milliseconds
223
+ // error: null,
224
+ // result: { records: 1250 }
225
+ // }
226
+
227
+ // Clear history for a job
228
+ await jobs.clearHistory('daily_report');
229
+ ```
230
+
231
+ ### Job Data & Results
232
+
233
+ ```typescript
234
+ const job = await jobs.schedule({
235
+ name: 'data_export',
236
+ schedule: '0 0 * * *',
237
+ handler: async (context) => {
238
+ const records = await exportData();
239
+
240
+ // Return result data
241
+ return {
242
+ recordCount: records.length,
243
+ fileSize: calculateSize(records),
244
+ exportedAt: new Date(),
245
+ };
246
+ },
247
+ });
248
+
249
+ // Get last execution result
250
+ const lastRun = await jobs.getLastExecution('data_export');
251
+ console.log('Last export:', lastRun.result);
252
+ ```
253
+
254
+ ## Common Patterns
255
+
256
+ ### Database Cleanup Job
257
+
258
+ ```typescript
259
+ jobs.schedule({
260
+ name: 'cleanup_old_records',
261
+ schedule: '0 2 * * *', // 2 AM daily
262
+ handler: async (context) => {
263
+ const db = context.kernel.getService('database');
264
+
265
+ // Delete records older than 90 days
266
+ const cutoff = new Date();
267
+ cutoff.setDate(cutoff.getDate() - 90);
268
+
269
+ await db.delete({
270
+ object: 'audit_log',
271
+ filters: [{ field: 'created_at', operator: 'lt', value: cutoff }],
272
+ });
273
+ },
274
+ });
275
+ ```
276
+
277
+ ### Report Generation Job
278
+
279
+ ```typescript
280
+ jobs.schedule({
281
+ name: 'weekly_sales_report',
282
+ schedule: '0 8 * * 1', // Mondays at 8 AM
283
+ handler: async (context) => {
284
+ const analytics = context.kernel.getService('analytics');
285
+
286
+ const data = await analytics.query({
287
+ object: 'order',
288
+ aggregations: [{ function: 'sum', field: 'amount' }],
289
+ groupBy: ['sales_rep'],
290
+ filters: [{ field: 'created_at', operator: 'last_week' }],
291
+ });
292
+
293
+ // Generate and email report
294
+ await sendReport(data);
295
+ },
296
+ });
297
+ ```
298
+
299
+ ### Cache Warming Job
300
+
301
+ ```typescript
302
+ jobs.scheduleInterval({
303
+ name: 'warm_cache',
304
+ interval: 15 * 60 * 1000, // Every 15 minutes
305
+ handler: async (context) => {
306
+ const cache = context.kernel.getService('cache');
307
+
308
+ // Pre-load frequently accessed data
309
+ const popularProducts = await getPopularProducts();
310
+ await cache.set('popular_products', popularProducts, { ttl: 900 });
311
+ },
312
+ });
313
+ ```
314
+
315
+ ## REST API Endpoints
316
+
317
+ ```
318
+ GET /api/v1/jobs # List all jobs
319
+ GET /api/v1/jobs/:name # Get job details
320
+ POST /api/v1/jobs/:name/run # Run job immediately
321
+ POST /api/v1/jobs/:name/stop # Stop job
322
+ POST /api/v1/jobs/:name/resume # Resume job
323
+ DELETE /api/v1/jobs/:name # Delete job
324
+ GET /api/v1/jobs/:name/history # Get execution history
325
+ ```
326
+
327
+ ## Best Practices
328
+
329
+ 1. **Idempotent Handlers**: Job handlers should be idempotent (safe to run multiple times)
330
+ 2. **Error Handling**: Always handle errors gracefully and log failures
331
+ 3. **Timeout Limits**: Set reasonable timeout limits for long-running jobs
332
+ 4. **Resource Limits**: Limit concurrent executions to avoid overloading the system
333
+ 5. **Monitoring**: Monitor job execution times and failure rates
334
+ 6. **Timezone Awareness**: Always specify timezone for cron jobs to avoid ambiguity
335
+ 7. **Cleanup**: Periodically delete old job history to save storage
336
+
337
+ ## Performance Considerations
338
+
339
+ - **Concurrency**: Limit concurrent jobs based on system resources
340
+ - **Job Duration**: Keep job execution time reasonable (< 5 minutes ideal)
341
+ - **History Size**: Limit history entries to prevent memory bloat
342
+ - **Batch Processing**: Process records in batches for large datasets
343
+
344
+ ## Contract Implementation
345
+
346
+ Implements `IJobService` from `@objectstack/spec/contracts`:
347
+
348
+ ```typescript
349
+ interface IJobService {
350
+ schedule(options: ScheduleOptions): Promise<Job>;
351
+ scheduleInterval(options: IntervalOptions): Promise<Job>;
352
+ scheduleOnce(options: OnceOptions): Promise<Job>;
353
+ getJob(name: string): Promise<Job>;
354
+ listJobs(filter?: JobFilter): Promise<Job[]>;
355
+ stopJob(name: string): Promise<void>;
356
+ resumeJob(name: string): Promise<void>;
357
+ deleteJob(name: string): Promise<void>;
358
+ runNow(name: string): Promise<JobExecution>;
359
+ getJobHistory(name: string, options?: HistoryOptions): Promise<JobExecution[]>;
360
+ }
361
+ ```
362
+
363
+ ## License
364
+
365
+ Apache-2.0
366
+
367
+ ## See Also
368
+
369
+ - [Cron Expression Generator](https://crontab.guru/)
370
+ - [@objectstack/spec/contracts](../../spec/src/contracts/)
371
+ - [Job Scheduling Guide](/content/docs/guides/jobs/)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-job",
3
- "version": "4.0.3",
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.3",
18
- "@objectstack/spec": "4.0.3"
17
+ "@objectstack/core": "4.0.5",
18
+ "@objectstack/spec": "4.0.5"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^25.6.0",
22
- "typescript": "^6.0.2",
23
- "vitest": "^4.1.4"
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",
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/service-job@4.0.3 build /home/runner/work/framework/framework/packages/services/service-job
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- ESM dist/index.js 3.99 KB
14
- ESM dist/index.js.map 10.88 KB
15
- ESM ⚡️ Build success in 69ms
16
- CJS dist/index.cjs 5.09 KB
17
- CJS dist/index.cjs.map 11.42 KB
18
- CJS ⚡️ Build success in 71ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 10242ms
21
- DTS dist/index.d.ts 3.60 KB
22
- DTS dist/index.d.cts 3.60 KB
package/CHANGELOG.md DELETED
@@ -1,169 +0,0 @@
1
- # @objectstack/service-job
2
-
3
- ## 4.0.3
4
-
5
- ### Patch Changes
6
-
7
- - @objectstack/spec@4.0.3
8
- - @objectstack/core@4.0.3
9
-
10
- ## 4.0.2
11
-
12
- ### Patch Changes
13
-
14
- - Updated dependencies [5f659e9]
15
- - @objectstack/spec@4.0.2
16
- - @objectstack/core@4.0.2
17
-
18
- ## 4.0.0
19
-
20
- ### Patch Changes
21
-
22
- - Updated dependencies [f08ffc3]
23
- - Updated dependencies [e0b0a78]
24
- - @objectstack/spec@4.0.0
25
- - @objectstack/core@4.0.0
26
-
27
- ## 3.3.1
28
-
29
- ### Patch Changes
30
-
31
- - @objectstack/spec@3.3.1
32
- - @objectstack/core@3.3.1
33
-
34
- ## 3.3.0
35
-
36
- ### Patch Changes
37
-
38
- - @objectstack/spec@3.3.0
39
- - @objectstack/core@3.3.0
40
-
41
- ## 3.2.9
42
-
43
- ### Patch Changes
44
-
45
- - @objectstack/spec@3.2.9
46
- - @objectstack/core@3.2.9
47
-
48
- ## 3.2.8
49
-
50
- ### Patch Changes
51
-
52
- - @objectstack/spec@3.2.8
53
- - @objectstack/core@3.2.8
54
-
55
- ## 3.2.7
56
-
57
- ### Patch Changes
58
-
59
- - @objectstack/spec@3.2.7
60
- - @objectstack/core@3.2.7
61
-
62
- ## 3.2.6
63
-
64
- ### Patch Changes
65
-
66
- - @objectstack/spec@3.2.6
67
- - @objectstack/core@3.2.6
68
-
69
- ## 3.2.5
70
-
71
- ### Patch Changes
72
-
73
- - @objectstack/spec@3.2.5
74
- - @objectstack/core@3.2.5
75
-
76
- ## 3.2.4
77
-
78
- ### Patch Changes
79
-
80
- - @objectstack/spec@3.2.4
81
- - @objectstack/core@3.2.4
82
-
83
- ## 3.2.3
84
-
85
- ### Patch Changes
86
-
87
- - @objectstack/spec@3.2.3
88
- - @objectstack/core@3.2.3
89
-
90
- ## 3.2.2
91
-
92
- ### Patch Changes
93
-
94
- - Updated dependencies [46defbb]
95
- - @objectstack/spec@3.2.2
96
- - @objectstack/core@3.2.2
97
-
98
- ## 3.2.1
99
-
100
- ### Patch Changes
101
-
102
- - Updated dependencies [850b546]
103
- - @objectstack/spec@3.2.1
104
- - @objectstack/core@3.2.1
105
-
106
- ## 3.2.0
107
-
108
- ### Patch Changes
109
-
110
- - Updated dependencies [5901c29]
111
- - @objectstack/spec@3.2.0
112
- - @objectstack/core@3.2.0
113
-
114
- ## 3.1.1
115
-
116
- ### Patch Changes
117
-
118
- - Updated dependencies [953d667]
119
- - @objectstack/spec@3.1.1
120
- - @objectstack/core@3.1.1
121
-
122
- ## 3.1.0
123
-
124
- ### Patch Changes
125
-
126
- - Updated dependencies [0088830]
127
- - @objectstack/spec@3.1.0
128
- - @objectstack/core@3.1.0
129
-
130
- ## 3.0.11
131
-
132
- ### Patch Changes
133
-
134
- - Updated dependencies [92d9d99]
135
- - @objectstack/spec@3.0.11
136
- - @objectstack/core@3.0.11
137
-
138
- ## 3.0.10
139
-
140
- ### Patch Changes
141
-
142
- - Updated dependencies [d1e5d31]
143
- - @objectstack/spec@3.0.10
144
- - @objectstack/core@3.0.10
145
-
146
- ## 3.0.9
147
-
148
- ### Patch Changes
149
-
150
- - Updated dependencies [15e0df6]
151
- - @objectstack/spec@3.0.9
152
- - @objectstack/core@3.0.9
153
-
154
- ## 3.0.8
155
-
156
- ### Patch Changes
157
-
158
- - Updated dependencies [5a968a2]
159
- - @objectstack/spec@3.0.8
160
- - @objectstack/core@3.0.8
161
-
162
- ## 3.0.7
163
-
164
- ### Patch Changes
165
-
166
- - Updated dependencies [0119bd7]
167
- - Updated dependencies [5426bdf]
168
- - @objectstack/spec@3.0.7
169
- - @objectstack/core@3.0.7
@@ -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
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": ["node"]
7
- },
8
- "include": ["src"],
9
- "exclude": ["node_modules", "dist"]
10
- }