@outloud/adonis-scheduler 1.0.6 → 1.1.0

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.
@@ -0,0 +1,36 @@
1
+ import { ErrorHandler, Factory, SchedulerConfig, TaskRegisterOptions } from "./types.js";
2
+ import { Task } from "./task.js";
3
+ import { ContainerResolver } from "@adonisjs/core/container";
4
+ import { ContainerBindings } from "@adonisjs/core/types";
5
+ import { LockService } from "@adonisjs/lock/types";
6
+ import { Logger } from "@adonisjs/core/logger";
7
+ import { Emitter } from "@adonisjs/core/events";
8
+
9
+ //#region src/scheduler.d.ts
10
+ declare class Scheduler {
11
+ private config;
12
+ private resolver;
13
+ private logger;
14
+ private emitter;
15
+ private locks?;
16
+ private definitions;
17
+ private state;
18
+ private errorHandler?;
19
+ constructor(config: SchedulerConfig, resolver: ContainerResolver<ContainerBindings>, logger: Logger, emitter: Emitter<any>, locks?: LockService);
20
+ register(options: TaskRegisterOptions | typeof Task | Factory<typeof Task>): this;
21
+ start(wait?: boolean): Promise<void>;
22
+ stop(): Promise<void>;
23
+ onError(callback: ErrorHandler): this;
24
+ private registerFromGlob;
25
+ private load;
26
+ private make;
27
+ private run;
28
+ private hasState;
29
+ private setState;
30
+ private handleError;
31
+ private cancel;
32
+ private terminate;
33
+ private schedule;
34
+ }
35
+ //#endregion
36
+ export { Scheduler };
@@ -0,0 +1,147 @@
1
+ import { Task } from "./task.js";
2
+ import { waitUntil } from "./helpers.js";
3
+ import { glob } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { Cron } from "croner";
6
+
7
+ //#region src/scheduler.ts
8
+ var Scheduler = class {
9
+ definitions = [];
10
+ state = "created";
11
+ errorHandler;
12
+ constructor(config, resolver, logger, emitter, locks) {
13
+ this.config = config;
14
+ this.resolver = resolver;
15
+ this.logger = logger;
16
+ this.emitter = emitter;
17
+ this.locks = locks;
18
+ }
19
+ register(options) {
20
+ const definition = {
21
+ schedule: "* * * * *",
22
+ state: "created",
23
+ jobs: []
24
+ };
25
+ if (typeof options === "object") {
26
+ const command = Array.isArray(options.command) ? options.command : [options.command];
27
+ Object.assign(definition, options);
28
+ definition.loader = () => import("./command.task.js").then((module) => {
29
+ return class extends module.CommandTask {
30
+ static command = command;
31
+ };
32
+ });
33
+ } else if (Task.isTask(options)) {
34
+ definition.task = options;
35
+ Object.assign(definition, options.options ?? {});
36
+ } else definition.loader = options;
37
+ if (!definition.task && !definition.loader) throw new Error("Task definition must have either a command or a task defined.");
38
+ this.definitions.push(definition);
39
+ if (this.hasState(["starting", "running"])) this.schedule(definition);
40
+ return this;
41
+ }
42
+ async start(wait = false) {
43
+ if (!this.hasState(["created", "stopped"])) {
44
+ this.logger.warn("Scheduler is already running");
45
+ return;
46
+ }
47
+ this.setState("starting");
48
+ if (this.config.locations) await Promise.all(this.config.locations.map((location) => this.registerFromGlob(location)));
49
+ await Promise.all(this.definitions.map((definition) => this.schedule(definition)));
50
+ if (!this.definitions.length) this.logger.warn("No tasks registered, scheduler will not run any jobs.");
51
+ this.setState("running");
52
+ if (wait) return waitUntil(() => this.state === "stopped");
53
+ }
54
+ async stop() {
55
+ if (this.state === "starting") await waitUntil(() => this.state !== "starting");
56
+ if (this.state !== "running") {
57
+ this.logger.warn("Scheduler is not running");
58
+ return;
59
+ }
60
+ this.setState("stopping");
61
+ await Promise.all(this.definitions.map((definition) => this.terminate(definition)));
62
+ this.setState("stopped");
63
+ }
64
+ onError(callback) {
65
+ this.errorHandler = callback;
66
+ return this;
67
+ }
68
+ async registerFromGlob(pattern) {
69
+ const normalizedPattern = pattern.replace(/\.(?:js|ts)$/, ".{js,ts}");
70
+ for await (const file of glob(normalizedPattern)) try {
71
+ const module = await import(`file://${resolve(file)}`);
72
+ if (Task.isTask(module.default)) this.register(module.default);
73
+ } catch (error) {
74
+ console.warn(`Failed to load task from ${file}:`, error);
75
+ }
76
+ }
77
+ async load(definition) {
78
+ if (definition.loader) {
79
+ const module = await definition.loader();
80
+ definition.task = "default" in module ? module.default : module;
81
+ }
82
+ if (!definition.task) throw new Error("Failed to load task, no loader or task provided.");
83
+ Object.assign(definition, definition.task.options ?? {});
84
+ }
85
+ async make(definition) {
86
+ return await this.resolver.make(definition.task);
87
+ }
88
+ async run(definition) {
89
+ const task = await this.make(definition);
90
+ const lockDuration = definition.lock && typeof definition.lock !== "boolean" ? definition.lock : this.config.lockDuration;
91
+ const lock = definition.lock ? this.locks?.createLock(`scheduler:${task.name}`, lockDuration) : void 0;
92
+ if (definition.lock && !this.locks) this.logger.warn("Lock is not available, install @adonisjs/lock to use task locking.");
93
+ if (lock) {
94
+ if (!await lock.acquireImmediately()) {
95
+ this.config.warnWhenLocked && this.logger.warn(`Task "${definition.task?.name}" is locked and cannot be run.`);
96
+ return;
97
+ }
98
+ }
99
+ try {
100
+ definition.jobs.push(task);
101
+ const promise = this.resolver.call(task, "run").finally(() => lock?.release());
102
+ task.promise = promise.catch(() => {});
103
+ await promise;
104
+ } catch (error) {
105
+ await this.handleError(error, definition, task);
106
+ } finally {
107
+ definition.jobs.splice(definition.jobs.indexOf(task), 1);
108
+ }
109
+ }
110
+ hasState(state) {
111
+ return (Array.isArray(state) ? state : [state]).includes(this.state);
112
+ }
113
+ setState(state) {
114
+ if (this.state === state) return;
115
+ this.state = state;
116
+ this.logger.info(`Scheduler: ${state}`);
117
+ }
118
+ async handleError(error, _, task) {
119
+ await task.onError?.(error);
120
+ this.emitter.emit("scheduler:error", {
121
+ error,
122
+ task
123
+ });
124
+ await this.errorHandler?.(error, task);
125
+ if (!this.emitter.hasListeners("scheduler:error") && !this.errorHandler) throw error;
126
+ }
127
+ async cancel(job) {
128
+ await job.cancel();
129
+ }
130
+ async terminate(definition) {
131
+ definition.cron?.stop();
132
+ definition.cron = void 0;
133
+ await Promise.all(definition.jobs.map((job) => this.cancel(job)));
134
+ definition.state = "created";
135
+ }
136
+ async schedule(definition) {
137
+ if (definition.state !== "created") return;
138
+ definition.state = "preparing";
139
+ if (!definition.task) await this.load(definition);
140
+ definition.cron = new Cron(definition.schedule, { timezone: definition.timeZone }, () => this.run(definition));
141
+ definition.state = "ready";
142
+ this.logger.debug(`Scheduler: Task "${definition.task?.name}" scheduled with "${definition.schedule}"`);
143
+ }
144
+ };
145
+
146
+ //#endregion
147
+ export { Scheduler };
@@ -0,0 +1,19 @@
1
+ import { TaskOptions } from "./types.js";
2
+
3
+ //#region src/task.d.ts
4
+ declare abstract class Task {
5
+ isCanceled: boolean;
6
+ promise?: Promise<any>;
7
+ static options: TaskOptions;
8
+ static isTask(obj: unknown): obj is typeof Task;
9
+ constructor(..._: any[]);
10
+ get name(): string;
11
+ abstract run(...args: any[]): Promise<void>;
12
+ cancel(): Promise<void>;
13
+ }
14
+ interface Task {
15
+ onCancel?(): Promise<void>;
16
+ onError?(error: Error): Promise<void>;
17
+ }
18
+ //#endregion
19
+ export { Task };
@@ -0,0 +1,22 @@
1
+ //#region src/task.ts
2
+ var Task = class Task {
3
+ isCanceled = false;
4
+ promise;
5
+ static options;
6
+ static isTask(obj) {
7
+ return typeof obj === "function" && obj.prototype instanceof Task;
8
+ }
9
+ constructor(..._) {}
10
+ get name() {
11
+ const Ctor = this.constructor;
12
+ return Ctor.options.name ?? Ctor.name;
13
+ }
14
+ async cancel() {
15
+ this.isCanceled = true;
16
+ await this.onCancel?.();
17
+ await this.promise;
18
+ }
19
+ };
20
+
21
+ //#endregion
22
+ export { Task };
@@ -0,0 +1,60 @@
1
+ import { Task } from "./task.js";
2
+ import { Cron } from "croner";
3
+
4
+ //#region src/types.d.ts
5
+ interface SchedulerConfig {
6
+ /**
7
+ * Should the scheduler start with HTTP server?
8
+ */
9
+ httpServer: boolean;
10
+ /**
11
+ * Warn when a task is locked and cannot be run
12
+ */
13
+ warnWhenLocked: boolean;
14
+ /**
15
+ * The default ttl for the lock.
16
+ */
17
+ lockDuration: number | string;
18
+ /** Locations for task auto-discovery */
19
+ locations?: string[];
20
+ }
21
+ interface TaskOptions {
22
+ /**
23
+ * A unique name for the task.
24
+ */
25
+ name?: string;
26
+ /**
27
+ * The pattern to when the task should run.
28
+ *
29
+ * See https://croner.56k.guru/usage/pattern/ for more information.
30
+ */
31
+ schedule: string;
32
+ /**
33
+ * Time zone to use for the task.
34
+ */
35
+ timeZone?: string;
36
+ /**
37
+ * Lock the task to prevent it from running concurrently.
38
+ *
39
+ * If a string or number is provided, it will be as ttl.
40
+ *
41
+ * @default false
42
+ */
43
+ lock?: boolean | number | string;
44
+ }
45
+ interface TaskRegisterOptions extends TaskOptions {
46
+ command: string | string[];
47
+ }
48
+ type ErrorHandler = (error: Error, task: Task) => (void | Promise<void>);
49
+ interface SchedulerEvents {
50
+ 'scheduler:error': {
51
+ error: Error;
52
+ task: Task;
53
+ };
54
+ }
55
+ type MaybePromise<T> = T | Promise<T>;
56
+ type Factory<T> = () => MaybePromise<{
57
+ default: T;
58
+ } | T>;
59
+ //#endregion
60
+ export { ErrorHandler, Factory, SchedulerConfig, SchedulerEvents, TaskOptions, TaskRegisterOptions };
@@ -9,6 +9,7 @@ const schedulerConfig = defineConfig({
9
9
  httpServer: env.get('SCHEDULER_HTTP_SERVER', false),
10
10
  warnWhenLocked: false,
11
11
  lockDuration: '10m',
12
+ locations: ['./app/**/*.task.js'],
12
13
  })
13
14
 
14
15
  export default schedulerConfig
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@outloud/adonis-scheduler",
3
3
  "type": "module",
4
- "version": "1.0.6",
5
- "description": "Schedule cron jobs in AdonisJS.",
4
+ "version": "1.1.0",
5
+ "description": "Schedule cron jobs in AdonisJS 6/7.",
6
6
  "author": "Outloud <hello@outloud.co>",
7
7
  "contributors": [
8
8
  "Andrej Adamcik"
@@ -13,22 +13,20 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/madebyoutloud/adonis-scheduler.git"
15
15
  },
16
- "publishConfig": {
17
- "tag": "latest",
18
- "access": "public"
19
- },
20
16
  "files": [
21
17
  "build"
22
18
  ],
23
19
  "exports": {
24
- ".": "./build/index.js",
20
+ ".": "./build/src/index.js",
25
21
  "./provider": "./build/providers/scheduler.provider.js",
26
22
  "./commands": "./build/commands/main.js",
23
+ "./service": "./build/services/main.js",
27
24
  "./services/*": "./build/services/*.js"
28
25
  },
26
+ "sideEffects": false,
29
27
  "peerDependencies": {
30
- "@adonisjs/core": "^6",
31
- "@adonisjs/lock": "^1"
28
+ "@adonisjs/core": "^6 || ^7",
29
+ "@adonisjs/lock": "^1 || ^2"
32
30
  },
33
31
  "peerDependenciesMeta": {
34
32
  "@adonisjs/lock": {
@@ -36,17 +34,18 @@
36
34
  }
37
35
  },
38
36
  "devDependencies": {
39
- "@adonisjs/assembler": "^7.8.2",
40
- "@adonisjs/core": "~6.19.0",
41
- "@adonisjs/lock": "^1.1.1",
37
+ "@adonisjs/assembler": "^8.0.0",
38
+ "@adonisjs/core": "~7.0.0",
39
+ "@adonisjs/lock": "^2.1.0",
42
40
  "@adonisjs/tsconfig": "^1.4.1",
43
- "@outloud/eslint-config": "^2.0.5",
41
+ "@outloud/eslint-config": "^2.1.3",
44
42
  "@swc/core": "^1.13.3",
45
43
  "@types/node": "^24.2.1",
46
44
  "copyfiles": "^2.4.1",
47
45
  "eslint": "^9.33.0",
48
46
  "reflect-metadata": "^0.2.2",
49
47
  "release-it": "^19.0.4",
48
+ "tsdown": "0.21.0-beta.2",
50
49
  "tsup": "~8.5.0",
51
50
  "typescript": "~5.9.2"
52
51
  },
@@ -61,12 +60,8 @@
61
60
  "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
62
61
  "pretest": "pnpm run lint",
63
62
  "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",
63
+ "build": "tsdown && pnpm run copy:templates && pnpm run index:commands",
68
64
  "release": "release-it",
69
- "index:commands": "adonis-kit index build/commands",
70
- "version": "pnpm run build"
65
+ "index:commands": "NODE_OPTIONS='--import=reflect-metadata' adonis-kit index build/commands"
71
66
  }
72
67
  }
@@ -1,25 +0,0 @@
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 };
@@ -1,185 +0,0 @@
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
- emitter;
21
- locks;
22
- definitions = [];
23
- state = "created";
24
- errorHandler;
25
- constructor(config, resolver, logger, emitter, locks) {
26
- this.config = config;
27
- this.resolver = resolver;
28
- this.logger = logger;
29
- this.emitter = emitter;
30
- this.locks = locks;
31
- }
32
- register(options) {
33
- const definition = {
34
- schedule: "* * * * *",
35
- state: "created",
36
- jobs: []
37
- };
38
- if (typeof options === "object") {
39
- const command = Array.isArray(options.command) ? options.command : [
40
- options.command
41
- ];
42
- Object.assign(definition, options);
43
- definition.loader = () => import('./command.task-GUNK3QFY.js').then((module) => {
44
- return class extends module.CommandTask {
45
- static command = command;
46
- };
47
- });
48
- } else {
49
- definition.loader = options;
50
- }
51
- if (!definition.loader) {
52
- throw new Error("Task definition must have either a command or a task defined.");
53
- }
54
- this.definitions.push(definition);
55
- if (this.hasState([
56
- "starting",
57
- "running"
58
- ])) {
59
- this.schedule(definition);
60
- }
61
- return this;
62
- }
63
- async start(wait = false) {
64
- if (!this.hasState([
65
- "created",
66
- "stopped"
67
- ])) {
68
- this.logger.warn("Scheduler is already running");
69
- return;
70
- }
71
- this.setState("starting");
72
- await Promise.all(this.definitions.map((definition) => this.schedule(definition)));
73
- if (!this.definitions.length) {
74
- this.logger.warn("No tasks registered, scheduler will not run any jobs.");
75
- }
76
- this.setState("running");
77
- if (wait) {
78
- return waitUntil(() => this.state === "stopped");
79
- }
80
- }
81
- async stop() {
82
- if (this.state === "starting") {
83
- await waitUntil(() => this.state !== "starting");
84
- }
85
- if (this.state !== "running") {
86
- this.logger.warn("Scheduler is not running");
87
- return;
88
- }
89
- this.setState("stopping");
90
- await Promise.all(this.definitions.map((definition) => this.terminate(definition)));
91
- this.setState("stopped");
92
- }
93
- onError(callback) {
94
- this.errorHandler = callback;
95
- return this;
96
- }
97
- async load(definition) {
98
- if (definition.loader) {
99
- const module = await definition.loader();
100
- definition.task = "default" in module ? module.default : module;
101
- }
102
- if (!definition.task) {
103
- throw new Error("Failed to load task, no loader or task provided.");
104
- }
105
- Object.assign(definition, definition.task.options ?? {});
106
- }
107
- async make(definition) {
108
- return await this.resolver.make(definition.task);
109
- }
110
- async run(definition) {
111
- const task = await this.make(definition);
112
- const lockDuration = definition.lock && typeof definition.lock !== "boolean" ? definition.lock : this.config.lockDuration;
113
- const lock = definition.lock ? this.locks?.createLock(`scheduler:${task.name}`, lockDuration) : void 0;
114
- if (definition.lock && !this.locks) {
115
- this.logger.warn("Lock is not available, install @adonisjs/lock to use task locking.");
116
- }
117
- if (lock) {
118
- const acquired = await lock.acquireImmediately();
119
- if (!acquired) {
120
- this.config.warnWhenLocked && this.logger.warn(`Task "${definition.task?.name}" is locked and cannot be run.`);
121
- return;
122
- }
123
- }
124
- try {
125
- definition.jobs.push(task);
126
- const promise = this.resolver.call(task, "run").finally(() => lock?.release());
127
- task.promise = promise.catch(() => {
128
- });
129
- await promise;
130
- } catch (error) {
131
- await this.handleError(error, definition, task);
132
- } finally {
133
- definition.jobs.splice(definition.jobs.indexOf(task), 1);
134
- }
135
- }
136
- hasState(state) {
137
- const states = Array.isArray(state) ? state : [
138
- state
139
- ];
140
- return states.includes(this.state);
141
- }
142
- setState(state) {
143
- if (this.state === state) {
144
- return;
145
- }
146
- this.state = state;
147
- this.logger.info(`Scheduler: ${state}`);
148
- }
149
- async handleError(error, _, task) {
150
- await task.onError?.(error);
151
- this.emitter.emit("scheduler:error", {
152
- error,
153
- task
154
- });
155
- await this.errorHandler?.(error, task);
156
- if (!this.emitter.hasListeners("scheduler:error") && !this.errorHandler) {
157
- throw error;
158
- }
159
- }
160
- async cancel(job) {
161
- await job.$cancel();
162
- }
163
- async terminate(definition) {
164
- definition.cron?.stop();
165
- definition.cron = void 0;
166
- await Promise.all(definition.jobs.map((job) => this.cancel(job)));
167
- definition.state = "created";
168
- }
169
- async schedule(definition) {
170
- if (definition.state !== "created") {
171
- return;
172
- }
173
- definition.state = "preparing";
174
- if (!definition.task) {
175
- await this.load(definition);
176
- }
177
- definition.cron = new Cron(definition.schedule, {
178
- timezone: definition.timeZone
179
- }, () => this.run(definition));
180
- definition.state = "ready";
181
- this.logger.debug(`Scheduler: Task "${definition.task?.name}" scheduled with "${definition.schedule}"`);
182
- }
183
- };
184
-
185
- export { Scheduler };
@@ -1,4 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
-
4
- export { __name };
@@ -1,22 +0,0 @@
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 };
package/build/index.d.ts DELETED
@@ -1,14 +0,0 @@
1
- import { S as SchedulerConfig } from './scheduler-DO35E0Fu.js';
2
- export { a as Scheduler, T as Task, b as TaskOptions } from './scheduler-DO35E0Fu.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
- import '@adonisjs/core/events';
9
-
10
- declare function configure(command: Configure): Promise<void>;
11
-
12
- declare function defineConfig<T extends SchedulerConfig>(config: T): T;
13
-
14
- export { SchedulerConfig, configure, defineConfig };
package/build/index.js DELETED
@@ -1,29 +0,0 @@
1
- export { Scheduler } from './chunk-FOVOD6FP.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 };