@outloud/adonis-scheduler 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025-present, Outloud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ <div align="center">
2
+ <h2><b>Adonis Scheduler</b></h2>
3
+
4
+ <p>
5
+
6
+ `@outloud/adonis-scheduler` is a cron job scheduler for [AdonisJS](https://adonisjs.com/).
7
+
8
+ </p>
9
+ </div>
10
+
11
+
12
+ <div align="center">
13
+
14
+ [![npm-image]][npm-url] [![license-image]][license-url]
15
+
16
+ </div>
17
+
18
+ ---
19
+ ## Features
20
+ - Define tasks with cron-like scheduling.
21
+ - Run tasks as standalone processes or as part of the HTTP server.
22
+ - Locking mechanism to prevent concurrent task execution.
23
+ - Cancellation support for long-running tasks.
24
+ - Graceful shutdown.
25
+ - Global and task-level error handling.
26
+
27
+ ## Getting Started
28
+
29
+ Install the package from the npm registry and configure it.
30
+
31
+ ```bash
32
+ node ace add @outloud/adonis-scheduler
33
+ ```
34
+
35
+ See `config/scheduler.ts` for available configuration options.
36
+
37
+ ## Usage
38
+
39
+ To make scheduler work, you must define and register tasks.
40
+
41
+ ### Define a task
42
+
43
+ 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.
44
+
45
+ ```ts
46
+ import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
47
+
48
+ export default class TestTask extends Task {
49
+ static options: TaskOptions = {
50
+ schedule: '* * * * *'
51
+ }
52
+
53
+ async run(): Promise<void> {
54
+ // Your task logic here
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Register a task
60
+
61
+ For task to run it must be registered in the scheduler. You can register tasks in two ways: using the `start/scheduler.ts` preloaded file or in a provider's `start` method.
62
+
63
+ Using `start/scheduler.ts` file.
64
+
65
+ ```ts
66
+ import scheduler from '@outloud/adonis-scheduler/services/main'
67
+
68
+ scheduler.register(() => import('../app/tasks/test.task.js'))
69
+ ```
70
+
71
+ Or using a provider.
72
+
73
+ ```ts
74
+ import type { ApplicationService } from '@adonisjs/core/types'
75
+ import scheduler from '@outloud/adonis-scheduler/services/main'
76
+
77
+ export default class AppProvider {
78
+ constructor(protected app: ApplicationService) {}
79
+
80
+ start() {
81
+ scheduler.register(() => import('../app/tasks/test.task.js'))
82
+ }
83
+ }
84
+
85
+ ```
86
+
87
+ You can also run other commands using scheduler without defining custom Task class.
88
+
89
+ ```ts
90
+ import scheduler from '@outloud/adonis-scheduler/services/main'
91
+
92
+ scheduler.register({
93
+ command: '<command-name>',
94
+ schedule: '* * * * *',
95
+ })
96
+ ```
97
+
98
+ ### Running the scheduler
99
+
100
+ The scheduler can be run as standalone process or as part of the HTTP server.
101
+
102
+ To run it as a **standalone process**, you can use the following command:
103
+
104
+ ```bash
105
+ node ace scheduler:run
106
+ ```
107
+
108
+ To run it as part of the **HTTP server**, set following env variable:
109
+
110
+ ```bash
111
+ SCHEDULER_HTTP_SERVER=true
112
+ ```
113
+
114
+ ## Locking
115
+
116
+ > [!NOTE]
117
+ > This requires [@adonisjs/lock](https://docs.adonisjs.com/guides/digging-deeper/locks) package to be installed and configured.
118
+
119
+ 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.
120
+
121
+ ```ts
122
+ import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
123
+
124
+ export default class TestTask extends Task {
125
+ static options: TaskOptions = {
126
+ schedule: '* * * * *',
127
+ lock: true // or value for lock ttl
128
+ }
129
+
130
+ async run(): Promise<void> {
131
+ // Your task logic here
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Cancellation
137
+
138
+ The package supports cancellation and graceful shutdown. You can add `onCancel` handler in your task or watch for `isCanceled` property.
139
+
140
+ ```ts
141
+ import { Task } from '@outloud/adonis-scheduler'
142
+
143
+ export default class TestTask extends Task {
144
+ async run(): Promise<void> {
145
+ while (!this.isCanceled) {
146
+ // Your task logic here
147
+ }
148
+ }
149
+
150
+ async onCancel(): Promise<void> {
151
+ // teardown running logic
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## Error handling
157
+
158
+ It's possible to globally handle errors for all your tasks or define custom error handler for each task.
159
+
160
+ 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.
161
+ This handler will run only if custom error handler is not defined in the task itself.
162
+
163
+ ```ts
164
+ import logger from '@adonisjs/core/services/logger'
165
+ import scheduler from '@outloud/adonis-scheduler/services/main'
166
+ import { Sentry } from '@rlanz/sentry'
167
+
168
+ scheduler.onError((error, task) => {
169
+ logger.error(error)
170
+ Sentry.captureException(error)
171
+ })
172
+ ```
173
+
174
+ Custom error handler can be defined in the task itself by implementing `onError` method.
175
+
176
+ ```ts
177
+ import { Task } from '@outloud/adonis-scheduler'
178
+
179
+ export default class TestTask extends Task {
180
+ async onError(error: Error): Promise<void> {
181
+ // handle error
182
+ }
183
+ }
184
+ ```
185
+
186
+ [npm-image]: https://img.shields.io/npm/v/@outloud/adonis-scheduler.svg?style=for-the-badge&logo=**npm**
187
+ [npm-url]: https://npmjs.org/package/@outloud/adonis-scheduler "npm"
188
+
189
+ [license-image]: https://img.shields.io/npm/l/@outloud/adonis-scheduler?color=blueviolet&style=for-the-badge
190
+ [license-url]: LICENSE "license"
@@ -0,0 +1,25 @@
1
+ import { __name } from './chunk-SHUYVCID.js';
2
+
3
+ // src/task.ts
4
+ var Task = class {
5
+ static {
6
+ __name(this, "Task");
7
+ }
8
+ isCanceled = false;
9
+ promise;
10
+ static options;
11
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
12
+ constructor(..._) {
13
+ }
14
+ get name() {
15
+ const Ctor = this.constructor;
16
+ return Ctor.options.name ?? Ctor.name;
17
+ }
18
+ async $cancel() {
19
+ this.isCanceled = true;
20
+ await this.onCancel?.();
21
+ await this.promise;
22
+ }
23
+ };
24
+
25
+ export { Task };
@@ -0,0 +1,183 @@
1
+ import { __name } from './chunk-SHUYVCID.js';
2
+ import { Cron } from 'croner';
3
+ import timers from 'timers/promises';
4
+
5
+ async function waitUntil(callback, interval = 50) {
6
+ while (!callback()) {
7
+ await timers.setTimeout(interval);
8
+ }
9
+ }
10
+ __name(waitUntil, "waitUntil");
11
+
12
+ // src/scheduler.ts
13
+ var Scheduler = class {
14
+ static {
15
+ __name(this, "Scheduler");
16
+ }
17
+ config;
18
+ resolver;
19
+ logger;
20
+ locks;
21
+ definitions = [];
22
+ state = "created";
23
+ errorHandler;
24
+ constructor(config, resolver, logger, locks) {
25
+ this.config = config;
26
+ this.resolver = resolver;
27
+ this.logger = logger;
28
+ this.locks = locks;
29
+ }
30
+ register(options) {
31
+ const definition = {
32
+ schedule: "* * * * *",
33
+ state: "created",
34
+ jobs: []
35
+ };
36
+ if (typeof options === "object") {
37
+ const command = Array.isArray(options.command) ? options.command : [
38
+ options.command
39
+ ];
40
+ Object.assign(definition, options);
41
+ definition.loader = () => import('./command.task-GUNK3QFY.js').then((module) => {
42
+ return class extends module.CommandTask {
43
+ static command = command;
44
+ };
45
+ });
46
+ } else {
47
+ definition.loader = options;
48
+ }
49
+ if (!definition.loader) {
50
+ throw new Error("Task definition must have either a command or a task defined.");
51
+ }
52
+ this.definitions.push(definition);
53
+ if (this.hasState([
54
+ "starting",
55
+ "running"
56
+ ])) {
57
+ this.schedule(definition);
58
+ }
59
+ return this;
60
+ }
61
+ async start(wait = false) {
62
+ if (!this.hasState([
63
+ "created",
64
+ "stopped"
65
+ ])) {
66
+ this.logger.warn("Scheduler is already running");
67
+ return;
68
+ }
69
+ this.setState("starting");
70
+ await Promise.all(this.definitions.map((definition) => this.schedule(definition)));
71
+ if (!this.definitions.length) {
72
+ this.logger.warn("No tasks registered, scheduler will not run any jobs.");
73
+ }
74
+ this.setState("running");
75
+ if (wait) {
76
+ return waitUntil(() => this.state === "stopped");
77
+ }
78
+ }
79
+ async stop() {
80
+ if (this.state === "starting") {
81
+ await waitUntil(() => this.state !== "starting");
82
+ }
83
+ if (this.state !== "running") {
84
+ this.logger.warn("Scheduler is not running");
85
+ return;
86
+ }
87
+ this.setState("stopping");
88
+ await Promise.all(this.definitions.map((definition) => this.terminate(definition)));
89
+ this.setState("stopped");
90
+ }
91
+ onError(callback) {
92
+ this.errorHandler = callback;
93
+ return this;
94
+ }
95
+ async load(definition) {
96
+ if (definition.loader) {
97
+ const module = await definition.loader();
98
+ definition.task = "default" in module ? module.default : module;
99
+ }
100
+ if (!definition.task) {
101
+ throw new Error("Failed to load task, no loader or task provided.");
102
+ }
103
+ Object.assign(definition, definition.task.options ?? {});
104
+ }
105
+ async make(definition) {
106
+ return await this.resolver.make(definition.task);
107
+ }
108
+ async run(definition) {
109
+ const task = await this.make(definition);
110
+ const lockDuration = definition.lock && typeof definition.lock !== "boolean" ? definition.lock : this.config.lockDuration;
111
+ const lock = definition.lock ? this.locks?.createLock(`scheduler:${task.name}`, lockDuration) : void 0;
112
+ if (definition.lock && !this.locks) {
113
+ this.logger.warn("Lock is not available, install @adonisjs/lock to use task locking.");
114
+ }
115
+ if (lock) {
116
+ const acquired = await lock.acquireImmediately();
117
+ if (!acquired) {
118
+ this.config.warnWhenLocked && this.logger.warn(`Task "${definition.task?.name}" is locked and cannot be run.`);
119
+ return;
120
+ }
121
+ }
122
+ try {
123
+ definition.jobs.push(task);
124
+ const promise = this.resolver.call(task, "run").finally(() => lock?.release());
125
+ task.promise = promise.catch(() => {
126
+ });
127
+ await promise;
128
+ } catch (error) {
129
+ await this.handleError(error, definition, task);
130
+ } finally {
131
+ definition.jobs.splice(definition.jobs.indexOf(task), 1);
132
+ }
133
+ }
134
+ hasState(state) {
135
+ const states = Array.isArray(state) ? state : [
136
+ state
137
+ ];
138
+ return states.includes(this.state);
139
+ }
140
+ setState(state) {
141
+ if (this.state === state) {
142
+ return;
143
+ }
144
+ this.state = state;
145
+ this.logger.info(`Scheduler: ${state}`);
146
+ }
147
+ async handleError(error, _, task) {
148
+ if (task && task.onError) {
149
+ await task.onError(error);
150
+ return;
151
+ }
152
+ if (this.errorHandler) {
153
+ await this.errorHandler(error, task);
154
+ return;
155
+ }
156
+ throw error;
157
+ }
158
+ async cancel(job) {
159
+ await job.$cancel();
160
+ }
161
+ async terminate(definition) {
162
+ definition.cron?.stop();
163
+ definition.cron = void 0;
164
+ await Promise.all(definition.jobs.map((job) => this.cancel(job)));
165
+ definition.state = "created";
166
+ }
167
+ async schedule(definition) {
168
+ if (definition.state !== "created") {
169
+ return;
170
+ }
171
+ definition.state = "preparing";
172
+ if (!definition.task) {
173
+ await this.load(definition);
174
+ }
175
+ definition.cron = new Cron(definition.schedule, {
176
+ timezone: definition.timeZone
177
+ }, () => this.run(definition));
178
+ definition.state = "ready";
179
+ this.logger.debug(`Scheduler: Task "${definition.task?.name}" scheduled with "${definition.schedule}"`);
180
+ }
181
+ };
182
+
183
+ export { Scheduler };
@@ -0,0 +1,4 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export { __name };
@@ -0,0 +1,22 @@
1
+ import { Task } from './chunk-7P6J5U6O.js';
2
+ import { __name } from './chunk-SHUYVCID.js';
3
+ import ace from '@adonisjs/core/services/ace';
4
+
5
+ var CommandTask = class extends Task {
6
+ static {
7
+ __name(this, "CommandTask");
8
+ }
9
+ static command = [];
10
+ get name() {
11
+ return this.constructor.command.join(",");
12
+ }
13
+ async run() {
14
+ const [name, ...args] = this.constructor.command;
15
+ if (!name) {
16
+ throw new Error("No command name provided.");
17
+ }
18
+ await ace.exec(name, args);
19
+ }
20
+ };
21
+
22
+ export { CommandTask };
@@ -0,0 +1 @@
1
+ {"commands":[{"commandName":"make:task","description":"Make a new task class","help":"","namespace":"make","aliases":[],"flags":[],"args":[{"name":"name","argumentName":"name","required":true,"description":"Name of the task","type":"string"}],"options":{"allowUnknownFlags":true},"filePath":"make_task.js"},{"commandName":"scheduler:run","description":"Run a scheduler","help":"","namespace":"scheduler","aliases":[],"flags":[],"args":[],"options":{"startApp":true,"staysAlive":true},"filePath":"scheduler_run.js"}],"version":1}
@@ -0,0 +1,4 @@
1
+ import { CommandMetaData, Command } from '@adonisjs/ace/types';
2
+
3
+ export function getMetaData(): Promise<CommandMetaData[]>
4
+ export function getCommand(metaData: CommandMetaData): Promise<Command | null>
@@ -0,0 +1,36 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ /**
4
+ * In-memory cache of commands after they have been loaded
5
+ */
6
+ let commandsMetaData
7
+
8
+ /**
9
+ * Reads the commands from the "./commands.json" file. Since, the commands.json
10
+ * file is generated automatically, we do not have to validate its contents
11
+ */
12
+ export async function getMetaData() {
13
+ if (commandsMetaData) {
14
+ return commandsMetaData
15
+ }
16
+
17
+ const commandsIndex = await readFile(new URL('./commands.json', import.meta.url), 'utf-8')
18
+ commandsMetaData = JSON.parse(commandsIndex).commands
19
+
20
+ return commandsMetaData
21
+ }
22
+
23
+ /**
24
+ * Imports the command by lookingup its path from the commands
25
+ * metadata
26
+ */
27
+ export async function getCommand(metaData) {
28
+ const commands = await getMetaData()
29
+ const command = commands.find(({ commandName }) => metaData.commandName === commandName)
30
+ if (!command) {
31
+ return null
32
+ }
33
+
34
+ const { default: commandConstructor } = await import(new URL(command.filePath, import.meta.url).href)
35
+ return commandConstructor
36
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import { CommandOptions } from '@adonisjs/core/types/ace';
3
+
4
+ declare class MakeTask extends BaseCommand {
5
+ static commandName: string;
6
+ static description: string;
7
+ static options: CommandOptions;
8
+ /**
9
+ * The name of the job file.
10
+ */
11
+ name: string;
12
+ /**
13
+ * Execute command
14
+ */
15
+ run(): Promise<void>;
16
+ }
17
+
18
+ export { MakeTask as default };
@@ -0,0 +1,42 @@
1
+ import { __name } from '../chunk-SHUYVCID.js';
2
+ import 'reflect-metadata';
3
+ import { BaseCommand, args } from '@adonisjs/core/ace';
4
+
5
+ function _ts_decorate(decorators, target, key, desc) {
6
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
8
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
10
+ }
11
+ __name(_ts_decorate, "_ts_decorate");
12
+ function _ts_metadata(k, v) {
13
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
14
+ }
15
+ __name(_ts_metadata, "_ts_metadata");
16
+ var MakeTask = class extends BaseCommand {
17
+ static {
18
+ __name(this, "MakeTask");
19
+ }
20
+ static commandName = "make:task";
21
+ static description = "Make a new task class";
22
+ static options = {
23
+ allowUnknownFlags: true
24
+ };
25
+ /**
26
+ * Execute command
27
+ */
28
+ async run() {
29
+ const codemods = await this.createCodemods();
30
+ await codemods.makeUsingStub(import.meta.dirname + "/../stubs", "command/task.stub", {
31
+ entity: this.app.generators.createEntity(this.name)
32
+ });
33
+ }
34
+ };
35
+ _ts_decorate([
36
+ args.string({
37
+ description: "Name of the task"
38
+ }),
39
+ _ts_metadata("design:type", String)
40
+ ], MakeTask.prototype, "name", void 0);
41
+
42
+ export { MakeTask as default };
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import { CommandOptions } from '@adonisjs/core/types/ace';
3
+ import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
4
+ import '@adonisjs/core/container';
5
+ import '@adonisjs/core/types';
6
+ import '@adonisjs/lock/types';
7
+ import '@adonisjs/core/logger';
8
+
9
+ declare class SchedulerRun extends BaseCommand {
10
+ static commandName: string;
11
+ static description: string;
12
+ static options: CommandOptions;
13
+ private scheduler?;
14
+ prepare(): void;
15
+ run(scheduler: Scheduler): Promise<void>;
16
+ }
17
+
18
+ export { SchedulerRun as default };
@@ -0,0 +1,46 @@
1
+ import { Scheduler } from '../chunk-BBI2I5B5.js';
2
+ import { __name } from '../chunk-SHUYVCID.js';
3
+ import 'reflect-metadata';
4
+ import { inject } from '@adonisjs/core';
5
+ import { BaseCommand } from '@adonisjs/core/ace';
6
+
7
+ function _ts_decorate(decorators, target, key, desc) {
8
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
10
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
11
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
12
+ }
13
+ __name(_ts_decorate, "_ts_decorate");
14
+ function _ts_metadata(k, v) {
15
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
16
+ }
17
+ __name(_ts_metadata, "_ts_metadata");
18
+ var SchedulerRun = class extends BaseCommand {
19
+ static {
20
+ __name(this, "SchedulerRun");
21
+ }
22
+ static commandName = "scheduler:run";
23
+ static description = "Run a scheduler";
24
+ static options = {
25
+ startApp: true,
26
+ staysAlive: true
27
+ };
28
+ scheduler;
29
+ prepare() {
30
+ this.app.terminating(() => this.scheduler?.stop());
31
+ }
32
+ async run(scheduler) {
33
+ this.scheduler = scheduler;
34
+ await this.scheduler.start(true);
35
+ }
36
+ };
37
+ _ts_decorate([
38
+ inject(),
39
+ _ts_metadata("design:type", Function),
40
+ _ts_metadata("design:paramtypes", [
41
+ typeof Scheduler === "undefined" ? Object : Scheduler
42
+ ]),
43
+ _ts_metadata("design:returntype", Promise)
44
+ ], SchedulerRun.prototype, "run", null);
45
+
46
+ export { SchedulerRun as default };
@@ -0,0 +1,13 @@
1
+ import { S as SchedulerConfig } from './scheduler-BWT0Iqko.js';
2
+ export { a as Scheduler, T as Task, b as TaskOptions } from './scheduler-BWT0Iqko.js';
3
+ import Configure from '@adonisjs/core/commands/configure';
4
+ import '@adonisjs/core/container';
5
+ import '@adonisjs/core/types';
6
+ import '@adonisjs/lock/types';
7
+ import '@adonisjs/core/logger';
8
+
9
+ declare function configure(command: Configure): Promise<void>;
10
+
11
+ declare function defineConfig<T extends SchedulerConfig>(config: T): T;
12
+
13
+ export { SchedulerConfig, configure, defineConfig };
package/build/index.js ADDED
@@ -0,0 +1,29 @@
1
+ export { Scheduler } from './chunk-BBI2I5B5.js';
2
+ export { Task } from './chunk-7P6J5U6O.js';
3
+ import { __name } from './chunk-SHUYVCID.js';
4
+
5
+ // configure.ts
6
+ async function configure(command) {
7
+ const codemods = await command.createCodemods();
8
+ await codemods.makeUsingStub(import.meta.dirname + "/stubs", "config/scheduler.stub", {});
9
+ await codemods.defineEnvVariables({
10
+ SCHEDULER_HTTP_SERVER: false
11
+ });
12
+ await codemods.defineEnvValidations({
13
+ variables: {
14
+ SCHEDULER_HTTP_SERVER: `Env.schema.boolean.optional()`
15
+ }
16
+ });
17
+ await codemods.updateRcFile((rcFile) => {
18
+ rcFile.addProvider("@outloud/adonis-scheduler/provider").addCommand("@outloud/adonis-scheduler/commands");
19
+ });
20
+ }
21
+ __name(configure, "configure");
22
+
23
+ // src/config.ts
24
+ function defineConfig(config) {
25
+ return config;
26
+ }
27
+ __name(defineConfig, "defineConfig");
28
+
29
+ export { configure, defineConfig };
@@ -0,0 +1,23 @@
1
+ import { ApplicationService } from '@adonisjs/core/types';
2
+ import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
3
+ import '@adonisjs/core/container';
4
+ import '@adonisjs/lock/types';
5
+ import '@adonisjs/core/logger';
6
+
7
+ declare class SchedulerProvider {
8
+ protected app: ApplicationService;
9
+ private scheduler?;
10
+ constructor(app: ApplicationService);
11
+ private getConfig;
12
+ private getLocks;
13
+ register(): void;
14
+ ready(): Promise<void>;
15
+ shutdown(): Promise<void>;
16
+ }
17
+ declare module '@adonisjs/core/types' {
18
+ interface ContainerBindings {
19
+ scheduler: Scheduler;
20
+ }
21
+ }
22
+
23
+ export { SchedulerProvider as default };
@@ -0,0 +1,40 @@
1
+ import { Scheduler } from '../chunk-BBI2I5B5.js';
2
+ import { __name } from '../chunk-SHUYVCID.js';
3
+
4
+ // providers/scheduler.provider.ts
5
+ var SchedulerProvider = class {
6
+ static {
7
+ __name(this, "SchedulerProvider");
8
+ }
9
+ app;
10
+ scheduler;
11
+ constructor(app) {
12
+ this.app = app;
13
+ }
14
+ getConfig() {
15
+ return this.app.config.get("scheduler", {});
16
+ }
17
+ async getLocks() {
18
+ if (this.app.container.hasBinding("lock.manager")) {
19
+ return await this.app.container.make("lock.manager");
20
+ }
21
+ }
22
+ register() {
23
+ this.app.container.singleton(Scheduler, async () => {
24
+ return new Scheduler(this.getConfig(), this.app.container.createResolver(), await this.app.container.make("logger"), await this.getLocks());
25
+ });
26
+ this.app.container.alias("scheduler", Scheduler);
27
+ }
28
+ async ready() {
29
+ const config = this.getConfig();
30
+ if (this.app.getEnvironment() === "web" && config.httpServer) {
31
+ this.scheduler = await this.app.container.make(Scheduler);
32
+ await this.scheduler.start();
33
+ }
34
+ }
35
+ async shutdown() {
36
+ await this.scheduler?.stop();
37
+ }
38
+ };
39
+
40
+ export { SchedulerProvider as default };
@@ -0,0 +1,91 @@
1
+ import { ContainerResolver } from '@adonisjs/core/container';
2
+ import { ContainerBindings } from '@adonisjs/core/types';
3
+ import { LockService } from '@adonisjs/lock/types';
4
+ import { Logger } from '@adonisjs/core/logger';
5
+
6
+ interface SchedulerConfig {
7
+ /**
8
+ * Should the scheduler start with HTTP server?
9
+ */
10
+ httpServer: boolean;
11
+ /**
12
+ * Warn when a task is locked and cannot be run
13
+ */
14
+ warnWhenLocked: boolean;
15
+ /**
16
+ * The default ttl for the lock.
17
+ */
18
+ lockDuration: number | string;
19
+ }
20
+ interface TaskOptions {
21
+ /**
22
+ * A unique name for the task.
23
+ */
24
+ name?: string;
25
+ /**
26
+ * The pattern to when the task should run.
27
+ *
28
+ * See https://croner.56k.guru/usage/pattern/ for more information.
29
+ */
30
+ schedule: string;
31
+ /**
32
+ * Time zone to use for the task.
33
+ */
34
+ timeZone?: string;
35
+ /**
36
+ * Lock the task to prevent it from running concurrently.
37
+ *
38
+ * If a string or number is provided, it will be as ttl.
39
+ *
40
+ * @default false
41
+ */
42
+ lock?: boolean | number | string;
43
+ }
44
+ interface TaskRegisterOptions extends TaskOptions {
45
+ command: string | string[];
46
+ }
47
+ type ErrorHandler = (error: Error, task: Task) => (void | Promise<void>);
48
+ type MaybePromise<T> = T | Promise<T>;
49
+ type Factory<T> = () => MaybePromise<{
50
+ default: T;
51
+ } | T>;
52
+
53
+ declare abstract class Task {
54
+ isCanceled: boolean;
55
+ promise?: Promise<any>;
56
+ static options: TaskOptions;
57
+ constructor(..._: any[]);
58
+ get name(): string;
59
+ abstract run(...args: any[]): Promise<void>;
60
+ $cancel(): Promise<void>;
61
+ }
62
+ interface Task {
63
+ onCancel?(): Promise<void>;
64
+ onError?(error: Error): Promise<void>;
65
+ }
66
+
67
+ declare class Scheduler {
68
+ private config;
69
+ private resolver;
70
+ private logger;
71
+ private locks?;
72
+ private definitions;
73
+ private state;
74
+ private errorHandler?;
75
+ constructor(config: SchedulerConfig, resolver: ContainerResolver<ContainerBindings>, logger: Logger, locks?: LockService | undefined);
76
+ register(options: TaskRegisterOptions | Factory<typeof Task>): this;
77
+ start(wait?: boolean): Promise<void>;
78
+ stop(): Promise<void>;
79
+ onError(callback: ErrorHandler): this;
80
+ private load;
81
+ private make;
82
+ private run;
83
+ private hasState;
84
+ private setState;
85
+ private handleError;
86
+ private cancel;
87
+ private terminate;
88
+ private schedule;
89
+ }
90
+
91
+ export { type SchedulerConfig as S, Task as T, Scheduler as a, type TaskOptions as b };
@@ -0,0 +1,9 @@
1
+ import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
2
+ import '@adonisjs/core/container';
3
+ import '@adonisjs/core/types';
4
+ import '@adonisjs/lock/types';
5
+ import '@adonisjs/core/logger';
6
+
7
+ declare let scheduler: Scheduler;
8
+
9
+ export { scheduler as default };
@@ -0,0 +1,9 @@
1
+ import '../chunk-SHUYVCID.js';
2
+ import app from '@adonisjs/core/services/app';
3
+
4
+ var scheduler;
5
+ await app?.booted(async () => {
6
+ scheduler = await app.container.make("scheduler");
7
+ });
8
+
9
+ export { scheduler as default };
@@ -0,0 +1,16 @@
1
+ {{#var name = string(entity.name).removeSuffix('task').suffix('Task').pascalCase().toString()}}
2
+ {{#var fileName = string(entity.name).removeSuffix('task').suffix('.task').snakeCase().removeExtension().ext('.ts').toString()}}
3
+ {{{
4
+ exports({ to: app.makePath('app/tasks', entity.path, fileName) })
5
+ }}}
6
+ import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
7
+
8
+ export default class {{ name }} extends Task {
9
+ static options: TaskOptions = {
10
+ schedule: '* * * * *'
11
+ }
12
+
13
+ async run(): Promise<void> {
14
+
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ {{{
2
+ exports({ to: app.configPath('scheduler.ts') })
3
+ }}}
4
+
5
+ import { defineConfig } from '@outloud/adonis-scheduler'
6
+ import env from '#start/env'
7
+
8
+ const schedulerConfig = defineConfig({
9
+ httpServer: env.get('SCHEDULER_HTTP_SERVER', false),
10
+ warnWhenLocked: false,
11
+ lockDuration: '10m',
12
+ })
13
+
14
+ export default schedulerConfig
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@outloud/adonis-scheduler",
3
+ "type": "module",
4
+ "version": "1.0.5",
5
+ "description": "Schedule cron jobs in AdonisJS.",
6
+ "author": "Outloud <hello@outloud.co>",
7
+ "contributors": [
8
+ "Andrej Adamcik"
9
+ ],
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/madebyoutloud/adonis-scheduler#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/madebyoutloud/adonis-scheduler.git"
15
+ },
16
+ "publishConfig": {
17
+ "tag": "latest",
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "build"
22
+ ],
23
+ "exports": {
24
+ ".": "./build/index.js",
25
+ "./provider": "./build/providers/scheduler.provider.js",
26
+ "./commands": "./build/commands/main.js",
27
+ "./services/*": "./build/services/*.js"
28
+ },
29
+ "peerDependencies": {
30
+ "@adonisjs/core": "^6",
31
+ "@adonisjs/lock": "^1"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@adonisjs/lock": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@adonisjs/assembler": "^7.8.2",
40
+ "@adonisjs/core": "~6.19.0",
41
+ "@adonisjs/lock": "^1.1.1",
42
+ "@adonisjs/tsconfig": "^1.4.1",
43
+ "@outloud/eslint-config": "^2.0.5",
44
+ "@swc/core": "^1.13.3",
45
+ "@types/node": "^24.2.1",
46
+ "copyfiles": "^2.4.1",
47
+ "eslint": "^9.33.0",
48
+ "reflect-metadata": "^0.2.2",
49
+ "release-it": "^19.0.4",
50
+ "tsup": "~8.5.0",
51
+ "typescript": "~5.9.2"
52
+ },
53
+ "dependencies": {
54
+ "croner": "^9"
55
+ },
56
+ "scripts": {
57
+ "copy:templates": "copyfiles \"stubs/**/*.stub\" build",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "eslint . ",
60
+ "lint:fix": "eslint --fix .",
61
+ "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
62
+ "pretest": "pnpm run lint",
63
+ "test": "c8 pnpm run quick:test",
64
+ "precompile": "pnpm run lint && rm -rf build",
65
+ "compile": "tsup-node",
66
+ "postcompile": "pnpm run copy:templates && pnpm run index:commands",
67
+ "build": "rm -rf build && pnpm run compile",
68
+ "release": "release-it",
69
+ "index:commands": "adonis-kit index build/commands",
70
+ "version": "pnpm run build"
71
+ }
72
+ }