Skip to content

Commit ea0274d

Browse files
authored
Provide mechanisms for awaiting async job execution (#236)
1 parent 37f5be1 commit ea0274d

File tree

10 files changed

+124
-16
lines changed

10 files changed

+124
-16
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ function createTask(job: AbstractBackgroundJob): AsyncTask {
294294
* `start(): void` - starts, or restarts (if it's already running) the job;
295295
* `stop(): void` - stops the job. Can be restarted again with `start` command;
296296
* `getStatus(): JobStatus` - returns the status of the job, which is one of: `running`, `stopped`.
297+
* `executeAsync: Promise<void>` - executes the job task once and awaits its completion. Next scheduled execution will still be executed on schedule. Respects the "preventOverrun" check.
298+
* `static createAndExecute(schedule: SimpleIntervalSchedule, task: Task | AsyncTask, options: JobOptions = {}): Promise<SimpleIntervalJob>` - creates and immediately executes the job, resolving only after execution has completed.
297299
298300
## API for scheduler
299301

index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { ToadScheduler } from './lib/toadScheduler'
2-
export { AsyncTask } from './lib/common/AsyncTask'
3-
export { Task } from './lib/common/Task'
2+
export { AsyncTask, isAsyncTask } from './lib/common/AsyncTask'
3+
export { Task, isSyncTask } from './lib/common/Task'
44
export { Job } from './lib/common/Job'
55
export { JobStatus } from './lib/common/Job'
66
export { SimpleIntervalJob } from './lib/engines/simple-interval/SimpleIntervalJob'

lib/common/AsyncTask.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { defaultErrorHandler, loggingErrorHandler } from './Logger'
22
import { isPromise } from './Utils'
3+
import { Task } from './Task'
4+
5+
export function isAsyncTask(task: Task | AsyncTask): task is AsyncTask {
6+
return (task as AsyncTask).isAsync === true
7+
}
38

49
export class AsyncTask {
10+
public isAsync = true
511
public isExecuting: boolean
612
private readonly id: string
713
private readonly handler: (taskId?: string, jobId?: string) => Promise<unknown>
@@ -19,8 +25,12 @@ export class AsyncTask {
1925
}
2026

2127
execute(jobId?: string): void {
28+
void this.executeAsync(jobId)
29+
}
30+
31+
executeAsync(jobId?: string): Promise<unknown> {
2232
this.isExecuting = true
23-
this.handler(this.id, jobId)
33+
return this.handler(this.id, jobId)
2434
.catch((err: Error) => {
2535
const errorHandleResult = this.errorHandler(err)
2636
if (isPromise(errorHandleResult)) {

lib/common/Task.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { defaultErrorHandler, loggingErrorHandler } from './Logger'
22
import { isPromise } from './Utils'
3+
import { AsyncTask } from './AsyncTask'
4+
5+
export function isSyncTask(task: Task | AsyncTask): task is Task {
6+
return (task as Task).isAsync === false
7+
}
38

49
export class Task {
10+
public isAsync = false
511
public isExecuting: boolean
612
private readonly id: string
713
private readonly handler: (taskId?: string, jobId?: string) => void

lib/engines/cron/CronJob.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class CronJob extends Job {
3939
}
4040

4141
start(): void {
42-
this.cronInstance = Cron(
42+
this.cronInstance = new Cron(
4343
this.schedule.cronExpression,
4444
{
4545
timezone: this.schedule.timezone,

lib/engines/simple-interval/SimpleIntervalJob.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Timeout = NodeJS.Timeout
2-
import { AsyncTask } from '../../common/AsyncTask'
2+
import { AsyncTask, isAsyncTask } from '../../common/AsyncTask'
33
import { Job, JobStatus } from '../../common/Job'
44
import { Task } from '../../common/Task'
55
import { SimpleIntervalSchedule, toMsecs } from './SimpleIntervalSchedule'
@@ -61,4 +61,24 @@ export class SimpleIntervalJob extends Job {
6161
}
6262
return JobStatus.STOPPED
6363
}
64+
65+
async executeAsync(): Promise<void> {
66+
if (!this.task.isExecuting || !this.preventOverrun) {
67+
if (isAsyncTask(this.task)) {
68+
await this.task.executeAsync(this.id)
69+
} else {
70+
this.task.execute(this.id)
71+
}
72+
}
73+
}
74+
75+
static async createAndExecute(schedule: SimpleIntervalSchedule, task: Task | AsyncTask, options: JobOptions = {}): Promise<SimpleIntervalJob> {
76+
const job = new SimpleIntervalJob({
77+
...schedule,
78+
runImmediately: false,
79+
}, task, options)
80+
81+
await job.executeAsync()
82+
return job
83+
}
6484
}

package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@
2222
"prepublishOnly": "npm run build"
2323
},
2424
"dependencies": {
25-
"croner": "^8.0.1"
25+
"croner": "^8.1.2"
2626
},
2727
"devDependencies": {
28-
"@types/jest": "^29.5.12",
29-
"@types/node": "^20.3.1",
30-
"@typescript-eslint/eslint-plugin": "^6.20.0",
31-
"@typescript-eslint/parser": "^6.20.0",
32-
"eslint": "^8.56.0",
33-
"jasmine-core": "^5.1.1",
28+
"@types/jest": "^29.5.14",
29+
"@types/node": "^20.17.32",
30+
"@typescript-eslint/eslint-plugin": "^7.18.0",
31+
"@typescript-eslint/parser": "^7.18.0",
32+
"eslint": "^8.57.1",
33+
"jasmine-core": "^5.7.1",
3434
"jest": "^29.7.0",
35-
"karma": "^6.4.2",
35+
"karma": "^6.4.4",
3636
"karma-chrome-launcher": "^3.2.0",
3737
"karma-jasmine": "^5.1.0",
3838
"karma-typescript": "^5.5.4",
3939
"prettier": "^3.0.0",
40-
"ts-jest": "^29.1.2",
40+
"ts-jest": "^29.3.2",
4141
"typescript": "5.2.2"
4242
},
4343
"homepage": "https://github.com/kibertoad/toad-scheduler",

test/SimpleIntervalJob.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { NoopTask } from './utils/testTasks'
55
import { advanceTimersByTime, mockTimers, unMockTimers } from './utils/timerUtils'
66
import { AsyncTask } from '../lib/common/AsyncTask'
77

8+
function sleep(ms: number) {
9+
return new Promise((resolve) => setTimeout(resolve, ms))
10+
}
11+
812
describe('ToadScheduler', () => {
913
beforeEach(() => {
1014
mockTimers()
@@ -411,5 +415,65 @@ describe('ToadScheduler', () => {
411415

412416
scheduler.stop()
413417
})
418+
419+
it('awaits until runImmediately completes when using createAndExecute with AsyncTask', async () => {
420+
unMockTimers()
421+
422+
let counter = 0
423+
const task = new AsyncTask('simple task', async () => {
424+
await sleep(200)
425+
counter++
426+
return Promise.resolve(undefined)
427+
})
428+
429+
const job = await SimpleIntervalJob.createAndExecute({
430+
days: 2,
431+
runImmediately: true,
432+
}, task)
433+
434+
expect(counter).toBe(1)
435+
job.stop()
436+
})
437+
438+
it('respects preventOverrun when using executeAsync with AsyncTask', async () => {
439+
unMockTimers()
440+
441+
let counter = 0
442+
const task = new AsyncTask('simple task', async () => {
443+
await sleep(300)
444+
counter++
445+
return Promise.resolve(undefined)
446+
})
447+
448+
const job = new SimpleIntervalJob({
449+
days: 2,
450+
runImmediately: true,
451+
}, task)
452+
453+
const promise1 = job.executeAsync()
454+
const promise2 = job.executeAsync()
455+
await Promise.all([promise1, promise2])
456+
457+
expect(counter).toBe(1)
458+
job.stop()
459+
})
460+
461+
it('awaits until runImmediately completes when using createAndExecute with Task', async () => {
462+
unMockTimers()
463+
464+
let counter = 0
465+
const task = new Task('simple task', async () => {
466+
counter++
467+
return Promise.resolve(undefined)
468+
})
469+
470+
const job = await SimpleIntervalJob.createAndExecute({
471+
days: 2,
472+
runImmediately: true,
473+
}, task)
474+
475+
expect(counter).toBe(1)
476+
job.stop()
477+
})
414478
})
415479
})

test/Task.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { ToadScheduler } from '../lib/toadScheduler'
22
import { SimpleIntervalJob } from '../lib/engines/simple-interval/SimpleIntervalJob'
3-
import { Task } from '../lib/common/Task'
3+
import { isSyncTask, Task } from '../lib/common/Task'
44
import { unMockTimers } from './utils/timerUtils'
55
import { expectAssertions } from './utils/assertUtils'
6+
import { AsyncTask } from '../lib/common/AsyncTask'
67

78
function sleep(ms: number) {
89
return new Promise((resolve) => setTimeout(resolve, ms))
910
}
1011

1112
describe('ToadScheduler', () => {
1213
describe('Task', () => {
14+
it('safeguard works', () => {
15+
expect(isSyncTask(new Task('id', () => {}))).toBe(true)
16+
expect(isSyncTask(new AsyncTask('id', () => Promise.resolve()))).toBe(false)
17+
})
18+
1319
it('correctly handles errors', (done) => {
1420
unMockTimers()
1521
expectAssertions(1)

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
"compilerOptions": {
33
"outDir": "dist",
44
"module": "commonjs",
5+
"moduleResolution": "node",
56
"target": "es2017",
67
"sourceMap": true,
78
"declaration": true,
89
"declarationMap": false,
910
"types": ["node", "jest"],
1011
"strict": true,
11-
"moduleResolution": "node",
1212
"noUnusedLocals": false,
1313
"noUnusedParameters": false,
1414
"noFallthroughCasesInSwitch": true,

0 commit comments

Comments
 (0)