@forinda/kickjs-cron 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Felix Orinda
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.
@@ -0,0 +1,193 @@
1
+ import { AppAdapter, Container } from '@forinda/kickjs-core';
2
+ export { CRON_META, Cron, CronJobMeta, getCronJobs } from '@forinda/kickjs-core';
3
+
4
+ /**
5
+ * Abstract scheduler backend. Implement this interface to use any
6
+ * cron library or timer strategy with the CronAdapter.
7
+ *
8
+ * KickJS ships two built-in implementations:
9
+ * - `CronerScheduler` — production-grade (requires `croner`)
10
+ * - `IntervalScheduler` — zero-dep fallback (setInterval-based)
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Custom scheduler using node-cron
15
+ * import cron from 'node-cron'
16
+ *
17
+ * class NodeCronScheduler implements CronScheduler {
18
+ * private tasks: cron.ScheduledTask[] = []
19
+ *
20
+ * schedule(expression, callback, options) {
21
+ * const task = cron.schedule(expression, callback, {
22
+ * timezone: options?.timezone,
23
+ * })
24
+ * this.tasks.push(task)
25
+ * return task
26
+ * }
27
+ *
28
+ * stop(handle) { handle.stop() }
29
+ * stopAll() { this.tasks.forEach(t => t.stop()); this.tasks = [] }
30
+ * }
31
+ * ```
32
+ */
33
+ interface CronScheduler {
34
+ /** Schedule a callback using a cron expression. Returns an opaque handle. */
35
+ schedule(expression: string, callback: () => void | Promise<void>, options?: {
36
+ timezone?: string;
37
+ }): any;
38
+ /** Stop a single scheduled job by its handle. */
39
+ stop(handle: any): void;
40
+ /** Stop all scheduled jobs. Called on shutdown. */
41
+ stopAll(): void;
42
+ }
43
+ /**
44
+ * Production-grade scheduler powered by `croner`.
45
+ *
46
+ * Features:
47
+ * - Full cron syntax: 5, 6, or 7 fields (with seconds and year)
48
+ * - OCPS 1.4 compliant
49
+ * - Timezone and DST support
50
+ * - Advanced patterns: L (last), W (weekday), # (nth occurrence)
51
+ * - Zero drift — fires at exact cron boundaries
52
+ *
53
+ * Requires `croner` as a peer dependency:
54
+ * ```bash
55
+ * pnpm add croner
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { CronAdapter, CronerScheduler } from '@forinda/kickjs-cron'
61
+ *
62
+ * new CronAdapter({
63
+ * services: [ReportService],
64
+ * scheduler: new CronerScheduler(),
65
+ * })
66
+ * ```
67
+ */
68
+ declare class CronerScheduler implements CronScheduler {
69
+ private CronClass;
70
+ private jobs;
71
+ /**
72
+ * @param cronerModule - Optional: pass the croner module directly to avoid
73
+ * dynamic import issues in bundlers. e.g. `new CronerScheduler(require('croner'))`
74
+ */
75
+ constructor(cronerModule?: {
76
+ Cron: any;
77
+ } | any);
78
+ /** @internal Load croner dynamically if not passed via constructor */
79
+ init(): Promise<void>;
80
+ schedule(expression: string, callback: () => void | Promise<void>, options?: {
81
+ timezone?: string;
82
+ }): any;
83
+ stop(handle: any): void;
84
+ stopAll(): void;
85
+ }
86
+ /**
87
+ * Lightweight fallback scheduler using `setInterval`.
88
+ * Converts common cron patterns to millisecond intervals.
89
+ *
90
+ * Use for development or simple recurring tasks. For production,
91
+ * use CronerScheduler or implement a custom CronScheduler.
92
+ *
93
+ * Limitations:
94
+ * - No timezone support
95
+ * - No day-of-week, day-of-month, or month-specific scheduling
96
+ * - Complex expressions fall back to 1-hour intervals
97
+ * - Timer drift over long periods
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { CronAdapter, IntervalScheduler } from '@forinda/kickjs-cron'
102
+ *
103
+ * new CronAdapter({
104
+ * services: [ReportService],
105
+ * scheduler: new IntervalScheduler(),
106
+ * })
107
+ * ```
108
+ */
109
+ declare class IntervalScheduler implements CronScheduler {
110
+ private timers;
111
+ schedule(expression: string, callback: () => void | Promise<void>, options?: {
112
+ timezone?: string;
113
+ }): NodeJS.Timeout;
114
+ stop(handle: NodeJS.Timeout): void;
115
+ stopAll(): void;
116
+ }
117
+
118
+ interface CronAdapterOptions {
119
+ /**
120
+ * Service classes that contain @Cron decorated methods.
121
+ * Must also be decorated with @Service() for DI resolution.
122
+ */
123
+ services: any[];
124
+ /** Enable/disable all cron jobs (default: true) */
125
+ enabled?: boolean;
126
+ /**
127
+ * Scheduler backend. Defaults to CronerScheduler (production-grade)
128
+ * with automatic fallback to IntervalScheduler if croner is not installed.
129
+ *
130
+ * Built-in:
131
+ * - `new CronerScheduler()` — full cron syntax, timezones, DST (requires `croner`)
132
+ * - `new IntervalScheduler()` — zero-dep, setInterval-based (dev/simple tasks)
133
+ *
134
+ * Or implement `CronScheduler` for node-cron, node-schedule, cloud schedulers, etc.
135
+ */
136
+ scheduler?: CronScheduler;
137
+ }
138
+ /**
139
+ * Cron adapter — scans services for @Cron decorated methods and
140
+ * schedules them using a pluggable scheduler backend.
141
+ *
142
+ * By default, tries `croner` for production-grade scheduling. Falls back
143
+ * to `setInterval` if croner is not installed.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { CronAdapter } from '@forinda/kickjs-cron'
148
+ *
149
+ * // Auto-detect best available scheduler
150
+ * bootstrap({
151
+ * adapters: [
152
+ * new CronAdapter({ services: [ReportService, CleanupService] }),
153
+ * ],
154
+ * })
155
+ *
156
+ * // Explicit production scheduler
157
+ * import { CronerScheduler } from '@forinda/kickjs-cron'
158
+ *
159
+ * bootstrap({
160
+ * adapters: [
161
+ * new CronAdapter({
162
+ * services: [ReportService],
163
+ * scheduler: new CronerScheduler(),
164
+ * }),
165
+ * ],
166
+ * })
167
+ *
168
+ * // Custom scheduler
169
+ * import { NodeCronScheduler } from './my-scheduler'
170
+ *
171
+ * bootstrap({
172
+ * adapters: [
173
+ * new CronAdapter({
174
+ * services: [ReportService],
175
+ * scheduler: new NodeCronScheduler(),
176
+ * }),
177
+ * ],
178
+ * })
179
+ * ```
180
+ */
181
+ declare class CronAdapter implements AppAdapter {
182
+ private options;
183
+ name: string;
184
+ private scheduler;
185
+ private enabled;
186
+ constructor(options: CronAdapterOptions);
187
+ afterStart(_server: any, container: Container): Promise<void>;
188
+ private resolveScheduler;
189
+ private runJob;
190
+ shutdown(): void;
191
+ }
192
+
193
+ export { CronAdapter, type CronAdapterOptions, type CronScheduler, CronerScheduler, IntervalScheduler };
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import "reflect-metadata";
6
+ import { Cron, getCronJobs as getCronJobs2, CRON_META } from "@forinda/kickjs-core";
7
+
8
+ // src/scheduler.ts
9
+ import { Logger } from "@forinda/kickjs-core";
10
+ var log = Logger.for("CronScheduler");
11
+ var CronerScheduler = class {
12
+ static {
13
+ __name(this, "CronerScheduler");
14
+ }
15
+ CronClass;
16
+ jobs = [];
17
+ /**
18
+ * @param cronerModule - Optional: pass the croner module directly to avoid
19
+ * dynamic import issues in bundlers. e.g. `new CronerScheduler(require('croner'))`
20
+ */
21
+ constructor(cronerModule) {
22
+ if (cronerModule) {
23
+ this.CronClass = cronerModule.Cron ?? cronerModule;
24
+ }
25
+ }
26
+ /** @internal Load croner dynamically if not passed via constructor */
27
+ async init() {
28
+ if (this.CronClass) return;
29
+ try {
30
+ const mod = await import("croner");
31
+ this.CronClass = mod.Cron ?? mod;
32
+ } catch {
33
+ throw new Error('CronerScheduler requires the "croner" package. Install it: pnpm add croner');
34
+ }
35
+ }
36
+ schedule(expression, callback, options) {
37
+ if (!this.CronClass) {
38
+ throw new Error("CronerScheduler not initialized. Ensure CronAdapter calls init() or pass the croner module to the constructor.");
39
+ }
40
+ const job = new this.CronClass(expression, {
41
+ timezone: options?.timezone,
42
+ protect: true
43
+ }, callback);
44
+ this.jobs.push(job);
45
+ return job;
46
+ }
47
+ stop(handle) {
48
+ handle?.stop?.();
49
+ this.jobs = this.jobs.filter((j) => j !== handle);
50
+ }
51
+ stopAll() {
52
+ for (const job of this.jobs) {
53
+ job?.stop?.();
54
+ }
55
+ this.jobs = [];
56
+ }
57
+ };
58
+ var IntervalScheduler = class {
59
+ static {
60
+ __name(this, "IntervalScheduler");
61
+ }
62
+ timers = [];
63
+ schedule(expression, callback, options) {
64
+ const intervalMs = cronToMs(expression);
65
+ if (intervalMs === null) {
66
+ throw new Error(`Invalid cron expression: "${expression}"`);
67
+ }
68
+ if (options?.timezone) {
69
+ log.warn("IntervalScheduler does not support timezones. Install croner and use CronerScheduler for timezone support.");
70
+ }
71
+ const timer = setInterval(async () => {
72
+ try {
73
+ await callback();
74
+ } catch {
75
+ }
76
+ }, intervalMs);
77
+ this.timers.push(timer);
78
+ return timer;
79
+ }
80
+ stop(handle) {
81
+ clearInterval(handle);
82
+ this.timers = this.timers.filter((t) => t !== handle);
83
+ }
84
+ stopAll() {
85
+ for (const timer of this.timers) {
86
+ clearInterval(timer);
87
+ }
88
+ this.timers = [];
89
+ }
90
+ };
91
+ function cronToMs(expression) {
92
+ const parts = expression.trim().split(/\s+/);
93
+ let minute, hour;
94
+ if (parts.length === 5) {
95
+ ;
96
+ [minute, hour] = parts;
97
+ } else if (parts.length === 6) {
98
+ ;
99
+ [, minute, hour] = parts;
100
+ } else {
101
+ return null;
102
+ }
103
+ if (parts.length === 6 && parts[0].startsWith("*/")) {
104
+ const secs = parseInt(parts[0].slice(2), 10);
105
+ if (!isNaN(secs) && secs > 0) return secs * 1e3;
106
+ }
107
+ if (minute.startsWith("*/")) {
108
+ const mins = parseInt(minute.slice(2), 10);
109
+ if (!isNaN(mins) && mins > 0) return mins * 60 * 1e3;
110
+ }
111
+ if (minute === "0" && hour === "*") return 60 * 60 * 1e3;
112
+ if (minute === "0" && hour.startsWith("*/")) {
113
+ const hrs = parseInt(hour.slice(2), 10);
114
+ if (!isNaN(hrs) && hrs > 0) return hrs * 60 * 60 * 1e3;
115
+ }
116
+ if (minute === "*" && hour === "*") return 60 * 1e3;
117
+ if (/^\d+$/.test(minute) && /^\d+$/.test(hour)) {
118
+ return 24 * 60 * 60 * 1e3;
119
+ }
120
+ log.warn(`Complex cron "${expression}" \u2014 using 1h interval. Install croner and use CronerScheduler for exact scheduling.`);
121
+ return 60 * 60 * 1e3;
122
+ }
123
+ __name(cronToMs, "cronToMs");
124
+
125
+ // src/adapter.ts
126
+ import { Logger as Logger2, getCronJobs } from "@forinda/kickjs-core";
127
+ var log2 = Logger2.for("CronAdapter");
128
+ var CronAdapter = class {
129
+ static {
130
+ __name(this, "CronAdapter");
131
+ }
132
+ options;
133
+ name = "CronAdapter";
134
+ scheduler = null;
135
+ enabled;
136
+ constructor(options) {
137
+ this.options = options;
138
+ this.enabled = options.enabled ?? true;
139
+ }
140
+ async afterStart(_server, container) {
141
+ if (!this.enabled) {
142
+ log2.info("Cron disabled");
143
+ return;
144
+ }
145
+ this.scheduler = await this.resolveScheduler();
146
+ let totalJobs = 0;
147
+ for (const ServiceClass of this.options.services) {
148
+ const jobs = getCronJobs(ServiceClass);
149
+ if (jobs.length === 0) continue;
150
+ const instance = container.resolve(ServiceClass);
151
+ for (const job of jobs) {
152
+ const label = job.description ?? `${ServiceClass.name}.${job.handlerName}`;
153
+ try {
154
+ if (job.runOnInit) {
155
+ this.runJob(instance, job, label);
156
+ }
157
+ this.scheduler.schedule(job.expression, () => this.runJob(instance, job, label), {
158
+ timezone: job.timezone
159
+ });
160
+ totalJobs++;
161
+ log2.info(`Scheduled: ${label} (${job.expression}${job.timezone ? ` [${job.timezone}]` : ""})`);
162
+ } catch (err) {
163
+ log2.error({
164
+ err
165
+ }, `Failed to schedule: ${label}`);
166
+ }
167
+ }
168
+ }
169
+ if (totalJobs > 0) {
170
+ const schedulerName = this.scheduler.constructor.name;
171
+ log2.info(`${totalJobs} cron job(s) active [${schedulerName}]`);
172
+ }
173
+ }
174
+ async resolveScheduler() {
175
+ if (this.options.scheduler) {
176
+ return this.options.scheduler;
177
+ }
178
+ const croner = new CronerScheduler();
179
+ try {
180
+ await croner.init();
181
+ log2.info("Using CronerScheduler (production)");
182
+ return croner;
183
+ } catch {
184
+ log2.warn("croner not installed \u2014 using IntervalScheduler (limited cron support). For production: pnpm add croner");
185
+ return new IntervalScheduler();
186
+ }
187
+ }
188
+ async runJob(instance, job, label) {
189
+ try {
190
+ await instance[job.handlerName]();
191
+ } catch (err) {
192
+ log2.error({
193
+ err
194
+ }, `Cron job failed: ${label}`);
195
+ }
196
+ }
197
+ shutdown() {
198
+ if (this.scheduler) {
199
+ this.scheduler.stopAll();
200
+ }
201
+ log2.info("All cron jobs stopped");
202
+ }
203
+ };
204
+ export {
205
+ CRON_META,
206
+ Cron,
207
+ CronAdapter,
208
+ CronerScheduler,
209
+ IntervalScheduler,
210
+ getCronJobs2 as getCronJobs
211
+ };
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/scheduler.ts","../src/adapter.ts"],"sourcesContent":["import 'reflect-metadata'\n\n// Re-export the @Cron decorator and metadata from core\nexport { Cron, getCronJobs, type CronJobMeta, CRON_META } from '@forinda/kickjs-core'\n\n// Scheduler interface + built-in implementations\nexport { type CronScheduler, CronerScheduler, IntervalScheduler } from './scheduler'\n\n// CronAdapter\nexport { CronAdapter, type CronAdapterOptions } from './adapter'\n","import { Logger } from '@forinda/kickjs-core'\n\nconst log = Logger.for('CronScheduler')\n\n// ── CronScheduler Interface ─────────────────────────────────────────────\n\n/**\n * Abstract scheduler backend. Implement this interface to use any\n * cron library or timer strategy with the CronAdapter.\n *\n * KickJS ships two built-in implementations:\n * - `CronerScheduler` — production-grade (requires `croner`)\n * - `IntervalScheduler` — zero-dep fallback (setInterval-based)\n *\n * @example\n * ```ts\n * // Custom scheduler using node-cron\n * import cron from 'node-cron'\n *\n * class NodeCronScheduler implements CronScheduler {\n * private tasks: cron.ScheduledTask[] = []\n *\n * schedule(expression, callback, options) {\n * const task = cron.schedule(expression, callback, {\n * timezone: options?.timezone,\n * })\n * this.tasks.push(task)\n * return task\n * }\n *\n * stop(handle) { handle.stop() }\n * stopAll() { this.tasks.forEach(t => t.stop()); this.tasks = [] }\n * }\n * ```\n */\nexport interface CronScheduler {\n /** Schedule a callback using a cron expression. Returns an opaque handle. */\n schedule(\n expression: string,\n callback: () => void | Promise<void>,\n options?: { timezone?: string },\n ): any\n\n /** Stop a single scheduled job by its handle. */\n stop(handle: any): void\n\n /** Stop all scheduled jobs. Called on shutdown. */\n stopAll(): void\n}\n\n// ── CronerScheduler (production) ────────────────────────────────────────\n\n/**\n * Production-grade scheduler powered by `croner`.\n *\n * Features:\n * - Full cron syntax: 5, 6, or 7 fields (with seconds and year)\n * - OCPS 1.4 compliant\n * - Timezone and DST support\n * - Advanced patterns: L (last), W (weekday), # (nth occurrence)\n * - Zero drift — fires at exact cron boundaries\n *\n * Requires `croner` as a peer dependency:\n * ```bash\n * pnpm add croner\n * ```\n *\n * @example\n * ```ts\n * import { CronAdapter, CronerScheduler } from '@forinda/kickjs-cron'\n *\n * new CronAdapter({\n * services: [ReportService],\n * scheduler: new CronerScheduler(),\n * })\n * ```\n */\nexport class CronerScheduler implements CronScheduler {\n private CronClass: any\n private jobs: any[] = []\n\n /**\n * @param cronerModule - Optional: pass the croner module directly to avoid\n * dynamic import issues in bundlers. e.g. `new CronerScheduler(require('croner'))`\n */\n constructor(cronerModule?: { Cron: any } | any) {\n if (cronerModule) {\n this.CronClass = cronerModule.Cron ?? cronerModule\n }\n }\n\n /** @internal Load croner dynamically if not passed via constructor */\n async init(): Promise<void> {\n if (this.CronClass) return\n try {\n const mod: any = await import('croner')\n this.CronClass = mod.Cron ?? mod\n } catch {\n throw new Error('CronerScheduler requires the \"croner\" package. Install it: pnpm add croner')\n }\n }\n\n schedule(\n expression: string,\n callback: () => void | Promise<void>,\n options?: { timezone?: string },\n ): any {\n if (!this.CronClass) {\n throw new Error(\n 'CronerScheduler not initialized. Ensure CronAdapter calls init() ' +\n 'or pass the croner module to the constructor.',\n )\n }\n const job = new this.CronClass(\n expression,\n { timezone: options?.timezone, protect: true },\n callback,\n )\n this.jobs.push(job)\n return job\n }\n\n stop(handle: any): void {\n handle?.stop?.()\n this.jobs = this.jobs.filter((j) => j !== handle)\n }\n\n stopAll(): void {\n for (const job of this.jobs) {\n job?.stop?.()\n }\n this.jobs = []\n }\n}\n\n// ── IntervalScheduler (zero-dep fallback) ───────────────────────────────\n\n/**\n * Lightweight fallback scheduler using `setInterval`.\n * Converts common cron patterns to millisecond intervals.\n *\n * Use for development or simple recurring tasks. For production,\n * use CronerScheduler or implement a custom CronScheduler.\n *\n * Limitations:\n * - No timezone support\n * - No day-of-week, day-of-month, or month-specific scheduling\n * - Complex expressions fall back to 1-hour intervals\n * - Timer drift over long periods\n *\n * @example\n * ```ts\n * import { CronAdapter, IntervalScheduler } from '@forinda/kickjs-cron'\n *\n * new CronAdapter({\n * services: [ReportService],\n * scheduler: new IntervalScheduler(),\n * })\n * ```\n */\nexport class IntervalScheduler implements CronScheduler {\n private timers: NodeJS.Timeout[] = []\n\n schedule(\n expression: string,\n callback: () => void | Promise<void>,\n options?: { timezone?: string },\n ): NodeJS.Timeout {\n const intervalMs = cronToMs(expression)\n if (intervalMs === null) {\n throw new Error(`Invalid cron expression: \"${expression}\"`)\n }\n if (options?.timezone) {\n log.warn(\n 'IntervalScheduler does not support timezones. ' +\n 'Install croner and use CronerScheduler for timezone support.',\n )\n }\n const timer = setInterval(async () => {\n try {\n await callback()\n } catch {\n // Swallowed — errors are handled by CronAdapter.runJob\n }\n }, intervalMs)\n this.timers.push(timer)\n return timer\n }\n\n stop(handle: NodeJS.Timeout): void {\n clearInterval(handle)\n this.timers = this.timers.filter((t) => t !== handle)\n }\n\n stopAll(): void {\n for (const timer of this.timers) {\n clearInterval(timer)\n }\n this.timers = []\n }\n}\n\n// ── Lightweight cron-to-ms parser ───────────────────────────────────────\n\n// Converts simplified cron expressions to millisecond intervals.\n// Used internally by IntervalScheduler.\nfunction cronToMs(expression: string): number | null {\n const parts = expression.trim().split(/\\s+/)\n\n let minute: string, hour: string\n if (parts.length === 5) {\n ;[minute, hour] = parts\n } else if (parts.length === 6) {\n ;[, minute, hour] = parts\n } else {\n return null\n }\n\n // Every N seconds: */N * * * * *\n if (parts.length === 6 && parts[0].startsWith('*/')) {\n const secs = parseInt(parts[0].slice(2), 10)\n if (!isNaN(secs) && secs > 0) return secs * 1000\n }\n\n // Every N minutes: */N * * * *\n if (minute.startsWith('*/')) {\n const mins = parseInt(minute.slice(2), 10)\n if (!isNaN(mins) && mins > 0) return mins * 60 * 1000\n }\n\n // Every hour: 0 * * * *\n if (minute === '0' && hour === '*') return 60 * 60 * 1000\n\n // Every N hours: 0 */N * * *\n if (minute === '0' && hour.startsWith('*/')) {\n const hrs = parseInt(hour.slice(2), 10)\n if (!isNaN(hrs) && hrs > 0) return hrs * 60 * 60 * 1000\n }\n\n // Every minute: * * * * *\n if (minute === '*' && hour === '*') return 60 * 1000\n\n // Daily: 0 0 * * * or 0 N * * *\n if (/^\\d+$/.test(minute) && /^\\d+$/.test(hour)) {\n return 24 * 60 * 60 * 1000\n }\n\n // Fallback\n log.warn(\n `Complex cron \"${expression}\" — using 1h interval. ` +\n 'Install croner and use CronerScheduler for exact scheduling.',\n )\n return 60 * 60 * 1000\n}\n","import {\n Logger,\n type AppAdapter,\n type Container,\n getCronJobs,\n type CronJobMeta,\n} from '@forinda/kickjs-core'\n\nimport { type CronScheduler, CronerScheduler, IntervalScheduler } from './scheduler'\n\nconst log = Logger.for('CronAdapter')\n\n// ── CronAdapter Options ─────────────────────────────────────────────────\n\nexport interface CronAdapterOptions {\n /**\n * Service classes that contain @Cron decorated methods.\n * Must also be decorated with @Service() for DI resolution.\n */\n services: any[]\n\n /** Enable/disable all cron jobs (default: true) */\n enabled?: boolean\n\n /**\n * Scheduler backend. Defaults to CronerScheduler (production-grade)\n * with automatic fallback to IntervalScheduler if croner is not installed.\n *\n * Built-in:\n * - `new CronerScheduler()` — full cron syntax, timezones, DST (requires `croner`)\n * - `new IntervalScheduler()` — zero-dep, setInterval-based (dev/simple tasks)\n *\n * Or implement `CronScheduler` for node-cron, node-schedule, cloud schedulers, etc.\n */\n scheduler?: CronScheduler\n}\n\n// ── CronAdapter ─────────────────────────────────────────────────────────\n\n/**\n * Cron adapter — scans services for @Cron decorated methods and\n * schedules them using a pluggable scheduler backend.\n *\n * By default, tries `croner` for production-grade scheduling. Falls back\n * to `setInterval` if croner is not installed.\n *\n * @example\n * ```ts\n * import { CronAdapter } from '@forinda/kickjs-cron'\n *\n * // Auto-detect best available scheduler\n * bootstrap({\n * adapters: [\n * new CronAdapter({ services: [ReportService, CleanupService] }),\n * ],\n * })\n *\n * // Explicit production scheduler\n * import { CronerScheduler } from '@forinda/kickjs-cron'\n *\n * bootstrap({\n * adapters: [\n * new CronAdapter({\n * services: [ReportService],\n * scheduler: new CronerScheduler(),\n * }),\n * ],\n * })\n *\n * // Custom scheduler\n * import { NodeCronScheduler } from './my-scheduler'\n *\n * bootstrap({\n * adapters: [\n * new CronAdapter({\n * services: [ReportService],\n * scheduler: new NodeCronScheduler(),\n * }),\n * ],\n * })\n * ```\n */\nexport class CronAdapter implements AppAdapter {\n name = 'CronAdapter'\n private scheduler: CronScheduler | null = null\n private enabled: boolean\n\n constructor(private options: CronAdapterOptions) {\n this.enabled = options.enabled ?? true\n }\n\n async afterStart(_server: any, container: Container): Promise<void> {\n if (!this.enabled) {\n log.info('Cron disabled')\n return\n }\n\n this.scheduler = await this.resolveScheduler()\n\n let totalJobs = 0\n\n for (const ServiceClass of this.options.services) {\n const jobs = getCronJobs(ServiceClass)\n if (jobs.length === 0) continue\n\n const instance = container.resolve(ServiceClass)\n\n for (const job of jobs) {\n const label = job.description ?? `${ServiceClass.name}.${job.handlerName}`\n\n try {\n if (job.runOnInit) {\n this.runJob(instance, job, label)\n }\n\n this.scheduler.schedule(job.expression, () => this.runJob(instance, job, label), {\n timezone: job.timezone,\n })\n\n totalJobs++\n log.info(\n `Scheduled: ${label} (${job.expression}${job.timezone ? ` [${job.timezone}]` : ''})`,\n )\n } catch (err: any) {\n log.error({ err }, `Failed to schedule: ${label}`)\n }\n }\n }\n\n if (totalJobs > 0) {\n const schedulerName = this.scheduler.constructor.name\n log.info(`${totalJobs} cron job(s) active [${schedulerName}]`)\n }\n }\n\n private async resolveScheduler(): Promise<CronScheduler> {\n if (this.options.scheduler) {\n return this.options.scheduler\n }\n\n // Try CronerScheduler first (production default)\n const croner = new CronerScheduler()\n try {\n await croner.init()\n log.info('Using CronerScheduler (production)')\n return croner\n } catch {\n log.warn(\n 'croner not installed — using IntervalScheduler (limited cron support). ' +\n 'For production: pnpm add croner',\n )\n return new IntervalScheduler()\n }\n }\n\n private async runJob(instance: any, job: CronJobMeta, label: string): Promise<void> {\n try {\n await instance[job.handlerName]()\n } catch (err: any) {\n log.error({ err }, `Cron job failed: ${label}`)\n }\n }\n\n shutdown(): void {\n if (this.scheduler) {\n this.scheduler.stopAll()\n }\n log.info('All cron jobs stopped')\n }\n}\n"],"mappings":"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","names":["Cron","getCronJobs","CRON_META","Logger","log","Logger","for","CronerScheduler","CronClass","jobs","cronerModule","Cron","init","mod","Error","schedule","expression","callback","options","job","timezone","protect","push","stop","handle","filter","j","stopAll","IntervalScheduler","timers","intervalMs","cronToMs","warn","timer","setInterval","clearInterval","t","parts","trim","split","minute","hour","length","startsWith","secs","parseInt","slice","isNaN","mins","hrs","test","Logger","getCronJobs","log","Logger","for","CronAdapter","name","scheduler","enabled","options","afterStart","_server","container","info","resolveScheduler","totalJobs","ServiceClass","services","jobs","getCronJobs","length","instance","resolve","job","label","description","handlerName","runOnInit","runJob","schedule","expression","timezone","err","error","schedulerName","croner","CronerScheduler","init","warn","IntervalScheduler","shutdown","stopAll"]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@forinda/kickjs-cron",
3
+ "version": "1.1.0",
4
+ "description": "Production-grade cron job scheduling with pluggable backends for KickJS",
5
+ "keywords": [
6
+ "kickjs",
7
+ "cron",
8
+ "scheduler",
9
+ "jobs",
10
+ "croner",
11
+ "node-cron",
12
+ "typescript",
13
+ "decorators",
14
+ "production",
15
+ "timezone"
16
+ ],
17
+ "type": "module",
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "reflect-metadata": "^0.2.2",
31
+ "@forinda/kickjs-core": "1.1.0"
32
+ },
33
+ "peerDependencies": {
34
+ "croner": "^9.0.0 || ^10.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "croner": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.5.2",
43
+ "croner": "^10.0.1",
44
+ "tsup": "^8.5.0",
45
+ "typescript": "^5.9.2",
46
+ "vitest": "^3.2.4"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "license": "MIT",
52
+ "author": "Felix Orinda",
53
+ "engines": {
54
+ "node": ">=20.0"
55
+ },
56
+ "homepage": "https://forinda.github.io/kick-js/",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/forinda/kick-js.git",
60
+ "directory": "packages/cron"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/forinda/kick-js/issues"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "dev": "tsup --watch",
68
+ "test": "vitest run",
69
+ "typecheck": "tsc --noEmit",
70
+ "clean": "rm -rf dist .turbo"
71
+ }
72
+ }