npm package: @outloud/adonis-scheduler
Description: Schedule cron jobs in AdonisJS 6/7.
Author: Outloud
Latest version: 1.1.0

# AdonisJS Scheduler

Cron job scheduler for [AdonisJS](https://adonisjs.com/) 6/7.

<p>

[![typescript-image]][typescript-url]
[![npm-image]][npm-url]
[![npm-download-image]][npm-download-url]
[![license-image]][license-url]

</p>

---
## Features
- Define tasks with cron-like scheduling.
- Run tasks as standalone processes or as part of the HTTP server.
- Locking mechanism to prevent concurrent task execution.
- Cancellation support for long-running tasks.
- Graceful shutdown.
- Global and task-level error handling.
- Auto-discovery of tasks.

## Getting Started

Install the package from the npm registry and configure it.

```bash
node ace add @outloud/adonis-scheduler
```

See `config/scheduler.ts` for available configuration options.

## Usage

To make scheduler work, you must define and register tasks.

### Define a task

You can create a task using `node ace make:task <task-name>` command. This will create a new task file in the `app/tasks` directory.

```ts
import { Task, type TaskOptions } from '@outloud/adonis-scheduler'

export default class TestTask extends Task {
  static options: TaskOptions = {
    schedule: '* * * * *'
  }

  async run(): Promise<void> {
    // Your task logic here
  }
}
```

### Register a task
For task to run it must be registered in the scheduler.

> [!NOTE]
> By default tasks are auto-discovered using the locations defined in config.

If you want to register tasks manually, you can register tasks in two ways: using the `start/scheduler.ts` preloaded file or in a provider's `start` method.

Using `start/scheduler.ts` file.

```ts
import scheduler from '@outloud/adonis-scheduler/services/main'

scheduler.register(() => import('../app/tasks/test.task.js'))
```

Or using a provider.

```ts
import type { ApplicationService } from '@adonisjs/core/types'
import scheduler from '@outloud/adonis-scheduler/services/main'

export default class AppProvider {
  constructor(protected app: ApplicationService) {}

  start() {
    scheduler.register(() => import('../app/tasks/test.task.js'))
  }
}

```

You can also run other commands using scheduler without defining custom Task class.

```ts
import scheduler from '@outloud/adonis-scheduler/services/main'

scheduler.register({
  command: '<command-name>',
  schedule: '* * * * *',
})
```

### Running the scheduler

The scheduler can be run as standalone process or as part of the HTTP server.

To run it as a **standalone process**, you can use the following command:

```bash
node ace scheduler:run
```

To run it as part of the **HTTP server**, set following env variable:

```bash
SCHEDULER_HTTP_SERVER=true
```

You can also create a custom worker command that runs the scheduler. This is useful if you want to add health checks or run other logic with the scheduler.

```ts [commands/worker.ts]
import { createServer, type Server as HttpServer } from 'node:http'
import { BaseCommand } from '@adonisjs/core/ace'
import { Server } from '@adonisjs/core/http'
import type { CommandOptions } from '@adonisjs/core/types/ace'
import { inject } from '@adonisjs/core'
import { Scheduler } from '@outloud/adonis-scheduler'
import type { ApplicationService } from '@adonisjs/core/types'

export default class extends BaseCommand {
  static commandName = 'worker'
  static description = 'Run a worker process.'

  static options: CommandOptions = {
    startApp: true,
    staysAlive: true,
  }

  private server?: HttpServer
  private scheduler?: Scheduler

  prepare() {
    this.app.terminating(() => this.server?.close())
    this.app.terminating(() => this.scheduler?.stop())
  }

  @inject()
  async run(scheduler: Scheduler): Promise<void> {
    this.scheduler = scheduler

    await Promise.all([
      this.startServer(),
      this.scheduler.start(true),
    ])
  }

  private async startServer() {
    const server = await this.makeServer()
    const httpServer = createServer(server.handle.bind(server))
    this.server = httpServer
    await server.boot()

    server.setNodeServer(httpServer)

    const host = process.env.HOST || '0.0.0.0'
    const port = Number(process.env.PORT || '3000')

    httpServer.once('listening', () => this.logger.info(`listening to http server, host: ${host}, port: ${port}`))

    return httpServer.listen(port, host)
  }

  private async makeServer() {
    const server = new Server(
      this.app,
      await this.app.container.make('encryption'),
      await this.app.container.make('emitter'),
      await this.app.container.make('logger'),
      this.app.config.get<any>('app.http'),
    )

    const router = server.getRouter()
    router.get('/health', () => ({ status: 'ok' }))

    return server
  }
}
```

## Locking

> [!NOTE]
> This requires [@adonisjs/lock](https://docs.adonisjs.com/guides/digging-deeper/locks) package to be installed and configured.

The scheduler supports locking to prevent multiple instances of the same task from running concurrently. You can enable locking by setting the `lock` option in the task options.

```ts
import { Task, type TaskOptions } from '@outloud/adonis-scheduler'

export default class TestTask extends Task {
  static options: TaskOptions = {
    schedule: '* * * * *',
    lock: true // or value for lock ttl
  }

  async run(): Promise<void> {
    // Your task logic here
  }
}
```

## Cancellation

The package supports cancellation and graceful shutdown. You can add `onCancel` handler in your task or watch for `isCanceled` property.

```ts
import { Task } from '@outloud/adonis-scheduler'

export default class TestTask extends Task {
  async run(): Promise<void> {
    while (!this.isCanceled) {
      // Your task logic here
    }
  }

  async onCancel(): Promise<void> {
    // teardown running logic
  }
}
```

## Error handling

It's possible to globally handle errors for all your tasks or define custom error handler for each task.

### Global error handler

To register global error handler, you can use the `onError` method of the scheduler service. You can define it in `start/scheduler.ts` preloaded file.
This handler will run only if custom error handler is not defined in the task itself.

```ts
import logger from '@adonisjs/core/services/logger'
import scheduler from '@outloud/adonis-scheduler/services/main'

scheduler.onError((error, task) => {
  logger.error(error)
})
```

Or you can listen to `scheduler:error` event using emitter.

```ts
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'

emitter.on('scheduler:error', ({ error, task }) => {
  logger.error(error)
})
```

> [!WARNING]
> When you register global error handler, the package will not throw any errors and it's your responsibility to log or handle them in the handler.
> If you don't register global error handler, the package will throw error and exit.

### Task-level error handler

Custom error handler can be defined in the task itself by implementing `onError` method.

```ts
import { Task } from '@outloud/adonis-scheduler'

export default class TestTask extends Task {
  async onError(error: Error): Promise<void> {
    // handle error
  }
}
```

[npm-image]: https://badgen.net/npm/v/@outloud/adonis-scheduler/latest
[npm-url]: https://npmjs.org/package/@outloud/adonis-scheduler "npm"

[npm-download-image]: https://badgen.net/npm/dm/@outloud/adonis-scheduler
[npm-download-url]: https://npmcharts.com/compare/@outloud/adonis-scheduler?minimal=true "downloads"

[typescript-image]: https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white
[typescript-url]: https://www.typescriptlang.org "TypeScript"

[license-image]: https://img.shields.io/npm/l/@outloud/adonis-scheduler.svg?sanitize=true
[license-url]: LICENSE.md "license"