Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ function createTask(job: AbstractBackgroundJob): AsyncTask {
* `start(): void` - starts, or restarts (if it's already running) the job;
* `stop(): void` - stops the job. Can be restarted again with `start` command;
* `getStatus(): JobStatus` - returns the status of the job, which is one of: `running`, `stopped`.
* `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.
* `static createAndExecute(schedule: SimpleIntervalSchedule, task: Task | AsyncTask, options: JobOptions = {}): Promise<SimpleIntervalJob>` - creates and immediately executes the job, resolving only after execution has completed.

## API for scheduler

Expand Down
4 changes: 2 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { ToadScheduler } from './lib/toadScheduler'
export { AsyncTask } from './lib/common/AsyncTask'
export { Task } from './lib/common/Task'
export { AsyncTask, isAsyncTask } from './lib/common/AsyncTask'
export { Task, isSyncTask } from './lib/common/Task'
export { Job } from './lib/common/Job'
export { JobStatus } from './lib/common/Job'
export { SimpleIntervalJob } from './lib/engines/simple-interval/SimpleIntervalJob'
Expand Down
12 changes: 11 additions & 1 deletion lib/common/AsyncTask.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { defaultErrorHandler, loggingErrorHandler } from './Logger'
import { isPromise } from './Utils'
import { Task } from './Task'

export function isAsyncTask(task: Task | AsyncTask): task is AsyncTask {
return (task as AsyncTask).isAsync === true
}

export class AsyncTask {
public isAsync = true
public isExecuting: boolean
private readonly id: string
private readonly handler: (taskId?: string, jobId?: string) => Promise<unknown>
Expand All @@ -19,8 +25,12 @@ export class AsyncTask {
}

execute(jobId?: string): void {
void this.executeAsync(jobId)
}

executeAsync(jobId?: string): Promise<unknown> {
this.isExecuting = true
this.handler(this.id, jobId)
return this.handler(this.id, jobId)
.catch((err: Error) => {
const errorHandleResult = this.errorHandler(err)
if (isPromise(errorHandleResult)) {
Expand Down
6 changes: 6 additions & 0 deletions lib/common/Task.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { defaultErrorHandler, loggingErrorHandler } from './Logger'
import { isPromise } from './Utils'
import { AsyncTask } from './AsyncTask'

export function isSyncTask(task: Task | AsyncTask): task is Task {
return (task as Task).isAsync === false
}

export class Task {
public isAsync = false
public isExecuting: boolean
private readonly id: string
private readonly handler: (taskId?: string, jobId?: string) => void
Expand Down
2 changes: 1 addition & 1 deletion lib/engines/cron/CronJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class CronJob extends Job {
}

start(): void {
this.cronInstance = Cron(
this.cronInstance = new Cron(
this.schedule.cronExpression,
{
timezone: this.schedule.timezone,
Expand Down
22 changes: 21 additions & 1 deletion lib/engines/simple-interval/SimpleIntervalJob.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Timeout = NodeJS.Timeout
import { AsyncTask } from '../../common/AsyncTask'
import { AsyncTask, isAsyncTask } from '../../common/AsyncTask'
import { Job, JobStatus } from '../../common/Job'
import { Task } from '../../common/Task'
import { SimpleIntervalSchedule, toMsecs } from './SimpleIntervalSchedule'
Expand Down Expand Up @@ -61,4 +61,24 @@ export class SimpleIntervalJob extends Job {
}
return JobStatus.STOPPED
}

async executeAsync(): Promise<void> {
if (!this.task.isExecuting || !this.preventOverrun) {
if (isAsyncTask(this.task)) {
await this.task.executeAsync(this.id)
} else {
this.task.execute(this.id)
}
}
}

static async createAndExecute(schedule: SimpleIntervalSchedule, task: Task | AsyncTask, options: JobOptions = {}): Promise<SimpleIntervalJob> {
const job = new SimpleIntervalJob({
...schedule,
runImmediately: false,
}, task, options)

await job.executeAsync()
return job
}
}
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"croner": "^8.0.1"
"croner": "^8.1.2"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.56.0",
"jasmine-core": "^5.1.1",
"@types/jest": "^29.5.14",
"@types/node": "^20.17.32",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.1",
"jasmine-core": "^5.7.1",
"jest": "^29.7.0",
"karma": "^6.4.2",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-jasmine": "^5.1.0",
"karma-typescript": "^5.5.4",
"prettier": "^3.0.0",
"ts-jest": "^29.1.2",
"ts-jest": "^29.3.2",
"typescript": "5.2.2"
},
"homepage": "https://github.com/kibertoad/toad-scheduler",
Expand Down
64 changes: 64 additions & 0 deletions test/SimpleIntervalJob.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { NoopTask } from './utils/testTasks'
import { advanceTimersByTime, mockTimers, unMockTimers } from './utils/timerUtils'
import { AsyncTask } from '../lib/common/AsyncTask'

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

describe('ToadScheduler', () => {
beforeEach(() => {
mockTimers()
Expand Down Expand Up @@ -411,5 +415,65 @@ describe('ToadScheduler', () => {

scheduler.stop()
})

it('awaits until runImmediately completes when using createAndExecute with AsyncTask', async () => {
unMockTimers()

let counter = 0
const task = new AsyncTask('simple task', async () => {
await sleep(200)
counter++
return Promise.resolve(undefined)
})

const job = await SimpleIntervalJob.createAndExecute({
days: 2,
runImmediately: true,
}, task)

expect(counter).toBe(1)
job.stop()
})

it('respects preventOverrun when using executeAsync with AsyncTask', async () => {
unMockTimers()

let counter = 0
const task = new AsyncTask('simple task', async () => {
await sleep(300)
counter++
return Promise.resolve(undefined)
})

const job = new SimpleIntervalJob({
days: 2,
runImmediately: true,
}, task)

const promise1 = job.executeAsync()
const promise2 = job.executeAsync()
await Promise.all([promise1, promise2])

expect(counter).toBe(1)
job.stop()
})

it('awaits until runImmediately completes when using createAndExecute with Task', async () => {
unMockTimers()

let counter = 0
const task = new Task('simple task', async () => {
counter++
return Promise.resolve(undefined)
})

const job = await SimpleIntervalJob.createAndExecute({
days: 2,
runImmediately: true,
}, task)

expect(counter).toBe(1)
job.stop()
})
})
})
8 changes: 7 additions & 1 deletion test/Task.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { ToadScheduler } from '../lib/toadScheduler'
import { SimpleIntervalJob } from '../lib/engines/simple-interval/SimpleIntervalJob'
import { Task } from '../lib/common/Task'
import { isSyncTask, Task } from '../lib/common/Task'
import { unMockTimers } from './utils/timerUtils'
import { expectAssertions } from './utils/assertUtils'
import { AsyncTask } from '../lib/common/AsyncTask'

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

describe('ToadScheduler', () => {
describe('Task', () => {
it('safeguard works', () => {
expect(isSyncTask(new Task('id', () => {}))).toBe(true)
expect(isSyncTask(new AsyncTask('id', () => Promise.resolve()))).toBe(false)
})

it('correctly handles errors', (done) => {
unMockTimers()
expectAssertions(1)
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"moduleResolution": "node",
"target": "es2017",
"sourceMap": true,
"declaration": true,
"declarationMap": false,
"types": ["node", "jest"],
"strict": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
Expand Down
Loading