@cinnabun/scheduler 0.0.1

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,5 @@
1
+ import type { ScheduledOptions } from "../interfaces/scheduler-options.js";
2
+ export declare function Scheduled(expressionOrOptions: string | (ScheduledOptions & {
3
+ fixedRate?: number;
4
+ fixedDelay?: number;
5
+ }), options?: ScheduledOptions): MethodDecorator;
@@ -0,0 +1,28 @@
1
+ import { schedulerMetadataStorage } from "../metadata/scheduler-storage.js";
2
+ function addScheduledMetadata(target, propertyKey, config) {
3
+ schedulerMetadataStorage.add(target.constructor, String(propertyKey), config);
4
+ }
5
+ export function Scheduled(expressionOrOptions, options) {
6
+ return (target, propertyKey, _descriptor) => {
7
+ if (typeof expressionOrOptions === "string") {
8
+ addScheduledMetadata(target, propertyKey, {
9
+ expression: expressionOrOptions,
10
+ options: options ? { name: options.name, timezone: options.timezone, enabled: options.enabled } : undefined,
11
+ });
12
+ }
13
+ else {
14
+ const opts = expressionOrOptions;
15
+ addScheduledMetadata(target, propertyKey, {
16
+ expression: undefined,
17
+ fixedRate: opts.fixedRate,
18
+ fixedDelay: opts.fixedDelay,
19
+ initialDelay: opts.initialDelay,
20
+ options: {
21
+ name: opts.name,
22
+ timezone: opts.timezone,
23
+ enabled: opts.enabled,
24
+ },
25
+ });
26
+ }
27
+ };
28
+ }
@@ -0,0 +1,6 @@
1
+ export type { ScheduledTask } from "./interfaces/scheduled-task.js";
2
+ export type { SchedulerModuleOptions, ScheduledOptions, } from "./interfaces/scheduler-options.js";
3
+ export { SchedulerService } from "./services/scheduler.service.js";
4
+ export { Scheduled } from "./decorators/scheduled.js";
5
+ export { SchedulerModule } from "./scheduler.module.js";
6
+ export { SchedulerPlugin } from "./scheduler.plugin.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { SchedulerService } from "./services/scheduler.service.js";
2
+ export { Scheduled } from "./decorators/scheduled.js";
3
+ export { SchedulerModule } from "./scheduler.module.js";
4
+ export { SchedulerPlugin } from "./scheduler.plugin.js";
@@ -0,0 +1,14 @@
1
+ export interface ScheduledTask {
2
+ name: string;
3
+ expression?: string;
4
+ handler: () => void | Promise<void>;
5
+ timezone?: string;
6
+ enabled: boolean;
7
+ lastRun?: Date;
8
+ nextRun?: Date;
9
+ /** For fixedRate/fixedDelay: interval in ms */
10
+ fixedRate?: number;
11
+ fixedDelay?: number;
12
+ /** Delay before first execution in ms */
13
+ initialDelay?: number;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export interface SchedulerModuleOptions {
2
+ timezone?: string;
3
+ enabled?: boolean;
4
+ }
5
+ export interface ScheduledOptions {
6
+ name?: string;
7
+ timezone?: string;
8
+ enabled?: boolean;
9
+ /** Spring-style: interval from start of each run (ms) */
10
+ fixedRate?: number;
11
+ /** Spring-style: interval from completion of previous run (ms) */
12
+ fixedDelay?: number;
13
+ /** Spring-style: delay before first execution (ms) */
14
+ initialDelay?: number;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ export interface ScheduledMetadata {
2
+ target: Function;
3
+ methodKey: string;
4
+ expression?: string;
5
+ fixedRate?: number;
6
+ fixedDelay?: number;
7
+ initialDelay?: number;
8
+ options?: {
9
+ name?: string;
10
+ timezone?: string;
11
+ enabled?: boolean;
12
+ };
13
+ }
14
+ declare class SchedulerMetadataStorage {
15
+ private tasks;
16
+ add(target: Function, methodKey: string, config: {
17
+ expression?: string;
18
+ fixedRate?: number;
19
+ fixedDelay?: number;
20
+ initialDelay?: number;
21
+ options?: {
22
+ name?: string;
23
+ timezone?: string;
24
+ enabled?: boolean;
25
+ };
26
+ }): void;
27
+ getAll(): ScheduledMetadata[];
28
+ reset(): void;
29
+ }
30
+ export declare const schedulerMetadataStorage: SchedulerMetadataStorage;
31
+ export {};
@@ -0,0 +1,21 @@
1
+ class SchedulerMetadataStorage {
2
+ tasks = [];
3
+ add(target, methodKey, config) {
4
+ this.tasks.push({
5
+ target,
6
+ methodKey,
7
+ expression: config.expression,
8
+ fixedRate: config.fixedRate,
9
+ fixedDelay: config.fixedDelay,
10
+ initialDelay: config.initialDelay,
11
+ options: config.options,
12
+ });
13
+ }
14
+ getAll() {
15
+ return [...this.tasks];
16
+ }
17
+ reset() {
18
+ this.tasks = [];
19
+ }
20
+ }
21
+ export const schedulerMetadataStorage = new SchedulerMetadataStorage();
@@ -0,0 +1,5 @@
1
+ import type { SchedulerModuleOptions } from "./interfaces/scheduler-options.js";
2
+ export declare class SchedulerModule {
3
+ static forRoot(options?: SchedulerModuleOptions): Function;
4
+ static getOptions(): SchedulerModuleOptions;
5
+ }
@@ -0,0 +1,33 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from "@cinnabun/core";
8
+ let moduleOptions = null;
9
+ export class SchedulerModule {
10
+ static forRoot(options = {}) {
11
+ moduleOptions = options;
12
+ let SchedulerDynamicModule = class SchedulerDynamicModule {
13
+ };
14
+ SchedulerDynamicModule = __decorate([
15
+ Module({
16
+ imports: [],
17
+ controllers: [],
18
+ providers: [],
19
+ exports: [],
20
+ })
21
+ ], SchedulerDynamicModule);
22
+ return SchedulerDynamicModule;
23
+ }
24
+ static getOptions() {
25
+ if (!moduleOptions) {
26
+ return { timezone: "UTC", enabled: true };
27
+ }
28
+ return {
29
+ timezone: moduleOptions.timezone ?? "UTC",
30
+ enabled: moduleOptions.enabled ?? true,
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ import type { CinnabunPlugin, PluginContext } from "@cinnabun/core";
2
+ export declare class SchedulerPlugin implements CinnabunPlugin {
3
+ name: string;
4
+ private schedulerService;
5
+ onInit(context: PluginContext): Promise<void>;
6
+ onReady(_context: PluginContext): Promise<void>;
7
+ onShutdown(_context: PluginContext): Promise<void>;
8
+ }
@@ -0,0 +1,26 @@
1
+ import { Logger } from "@cinnabun/core";
2
+ import { SchedulerModule } from "./scheduler.module.js";
3
+ import { SchedulerService } from "./services/scheduler.service.js";
4
+ export class SchedulerPlugin {
5
+ name = "SchedulerPlugin";
6
+ schedulerService = null;
7
+ async onInit(context) {
8
+ const logger = new Logger("SchedulerPlugin");
9
+ const options = SchedulerModule.getOptions();
10
+ const schedulerService = new SchedulerService(context.container);
11
+ context.container.registerInstance(SchedulerService, schedulerService);
12
+ this.schedulerService = schedulerService;
13
+ logger.info(`Scheduler module initialized (timezone: ${options.timezone}, enabled: ${options.enabled})`);
14
+ }
15
+ async onReady(_context) {
16
+ if (this.schedulerService) {
17
+ this.schedulerService.start();
18
+ }
19
+ }
20
+ async onShutdown(_context) {
21
+ if (this.schedulerService) {
22
+ this.schedulerService.stop();
23
+ this.schedulerService = null;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { Container } from "@cinnabun/core";
2
+ import type { ScheduledTask } from "../interfaces/scheduled-task.js";
3
+ export declare class SchedulerService {
4
+ private readonly tasks;
5
+ private readonly container;
6
+ private started;
7
+ constructor(container: Container);
8
+ schedule(task: ScheduledTask): void;
9
+ unschedule(name: string): void;
10
+ getTasks(): ScheduledTask[];
11
+ start(): void;
12
+ stop(): void;
13
+ }
@@ -0,0 +1,157 @@
1
+ import { schedulerMetadataStorage } from "../metadata/scheduler-storage.js";
2
+ import { expandCronExpression } from "../utils/cron-expand.js";
3
+ import { SchedulerModule } from "../scheduler.module.js";
4
+ export class SchedulerService {
5
+ tasks = new Map();
6
+ container;
7
+ started = false;
8
+ constructor(container) {
9
+ this.container = container;
10
+ }
11
+ schedule(task) {
12
+ if (!task.enabled)
13
+ return;
14
+ const existing = this.tasks.get(task.name);
15
+ if (existing?.cronRef) {
16
+ existing.cronRef.stop();
17
+ }
18
+ if (existing?.intervalRef) {
19
+ clearInterval(existing.intervalRef);
20
+ }
21
+ if (existing?.timeoutRef) {
22
+ clearTimeout(existing.timeoutRef);
23
+ }
24
+ const taskWithRef = { ...task };
25
+ this.tasks.set(task.name, taskWithRef);
26
+ const runHandler = async () => {
27
+ try {
28
+ await task.handler();
29
+ taskWithRef.lastRun = new Date();
30
+ }
31
+ catch (err) {
32
+ console.error(`[Scheduler] Task "${task.name}" failed:`, err);
33
+ }
34
+ };
35
+ if (task.fixedRate !== undefined) {
36
+ const run = () => {
37
+ runHandler();
38
+ };
39
+ const delay = task.initialDelay ?? 0;
40
+ const schedule = () => {
41
+ taskWithRef.intervalRef = setInterval(run, task.fixedRate);
42
+ };
43
+ if (delay > 0) {
44
+ setTimeout(schedule, delay);
45
+ }
46
+ else {
47
+ schedule();
48
+ }
49
+ return;
50
+ }
51
+ if (task.fixedDelay !== undefined) {
52
+ const run = async () => {
53
+ await runHandler();
54
+ taskWithRef.timeoutRef = setTimeout(run, task.fixedDelay);
55
+ };
56
+ const delay = task.initialDelay ?? 0;
57
+ if (delay > 0) {
58
+ taskWithRef.timeoutRef = setTimeout(run, delay);
59
+ }
60
+ else {
61
+ run();
62
+ }
63
+ return;
64
+ }
65
+ if (task.expression) {
66
+ const expansion = expandCronExpression(task.expression);
67
+ if (expansion.type === "interval") {
68
+ const run = () => runHandler();
69
+ const delay = task.initialDelay ?? 0;
70
+ const schedule = () => {
71
+ taskWithRef.intervalRef = setInterval(run, expansion.intervalMs);
72
+ };
73
+ if (delay > 0) {
74
+ setTimeout(schedule, delay);
75
+ }
76
+ else {
77
+ schedule();
78
+ }
79
+ return;
80
+ }
81
+ try {
82
+ const cron = require("node-cron");
83
+ const timezone = task.timezone ?? SchedulerModule.getOptions().timezone;
84
+ taskWithRef.cronRef = cron.schedule(expansion.expression, () => runHandler(), timezone ? { timezone } : undefined);
85
+ }
86
+ catch (err) {
87
+ console.error(`[Scheduler] Failed to schedule cron "${task.expression}" for "${task.name}":`, err);
88
+ }
89
+ }
90
+ }
91
+ unschedule(name) {
92
+ const task = this.tasks.get(name);
93
+ if (!task)
94
+ return;
95
+ if (task.cronRef) {
96
+ task.cronRef.stop();
97
+ task.cronRef = undefined;
98
+ }
99
+ if (task.intervalRef) {
100
+ clearInterval(task.intervalRef);
101
+ task.intervalRef = undefined;
102
+ }
103
+ if (task.timeoutRef) {
104
+ clearTimeout(task.timeoutRef);
105
+ task.timeoutRef = undefined;
106
+ }
107
+ this.tasks.delete(name);
108
+ }
109
+ getTasks() {
110
+ return Array.from(this.tasks.values()).map(({ cronRef, intervalRef, timeoutRef, ...t }) => t);
111
+ }
112
+ start() {
113
+ if (this.started)
114
+ return;
115
+ const options = SchedulerModule.getOptions();
116
+ if (options.enabled === false)
117
+ return;
118
+ const allMeta = schedulerMetadataStorage.getAll();
119
+ const timezone = options.timezone ?? "UTC";
120
+ for (const meta of allMeta) {
121
+ let instance;
122
+ try {
123
+ instance = this.container.resolve(meta.target);
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ const method = instance[meta.methodKey];
129
+ if (typeof method !== "function")
130
+ continue;
131
+ const enabled = meta.options?.enabled ?? true;
132
+ const taskName = meta.options?.name ?? `${meta.target.name}.${meta.methodKey}`;
133
+ const taskTimezone = meta.options?.timezone ?? timezone;
134
+ const handler = () => method.call(instance);
135
+ const task = {
136
+ name: taskName,
137
+ enabled,
138
+ handler,
139
+ timezone: taskTimezone,
140
+ expression: meta.expression,
141
+ fixedRate: meta.fixedRate,
142
+ fixedDelay: meta.fixedDelay,
143
+ initialDelay: meta.initialDelay,
144
+ };
145
+ this.schedule(task);
146
+ }
147
+ this.started = true;
148
+ }
149
+ stop() {
150
+ if (!this.started)
151
+ return;
152
+ for (const [name] of this.tasks) {
153
+ this.unschedule(name);
154
+ }
155
+ this.started = false;
156
+ }
157
+ }
@@ -0,0 +1,8 @@
1
+ export type CronExpansionResult = {
2
+ type: "cron";
3
+ expression: string;
4
+ } | {
5
+ type: "interval";
6
+ intervalMs: number;
7
+ };
8
+ export declare function expandCronExpression(input: string): CronExpansionResult;
@@ -0,0 +1,38 @@
1
+ const SHORTCUTS = {
2
+ "@yearly": "0 0 1 1 *",
3
+ "@annually": "0 0 1 1 *",
4
+ "@monthly": "0 0 1 * *",
5
+ "@weekly": "0 0 * * 0",
6
+ "@daily": "0 0 * * *",
7
+ "@midnight": "0 0 * * *",
8
+ "@hourly": "0 * * * *",
9
+ };
10
+ const EVERY_REGEX = /^@every\s+(\d+)(s|m|h|d)$/i;
11
+ export function expandCronExpression(input) {
12
+ const trimmed = input.trim();
13
+ const shortcut = SHORTCUTS[trimmed];
14
+ if (shortcut) {
15
+ return { type: "cron", expression: shortcut };
16
+ }
17
+ const everyMatch = trimmed.match(EVERY_REGEX);
18
+ if (everyMatch) {
19
+ const value = parseInt(everyMatch[1], 10);
20
+ const unit = everyMatch[2].toLowerCase();
21
+ if (value <= 0) {
22
+ throw new Error(`Invalid @every value: ${value}`);
23
+ }
24
+ switch (unit) {
25
+ case "s":
26
+ return { type: "interval", intervalMs: value * 1000 };
27
+ case "m":
28
+ return { type: "cron", expression: `*/${value} * * * *` };
29
+ case "h":
30
+ return { type: "cron", expression: `0 */${value} * * *` };
31
+ case "d":
32
+ return { type: "cron", expression: `0 0 */${value} * *` };
33
+ default:
34
+ throw new Error(`Unknown @every unit: ${unit}`);
35
+ }
36
+ }
37
+ return { type: "cron", expression: trimmed };
38
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@cinnabun/scheduler",
3
+ "version": "0.0.1",
4
+ "description": "Task scheduler for Cinnabun with cron and Spring-style fixedRate/fixedDelay",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "test": "bun test",
14
+ "build": "tsc",
15
+ "prepublishOnly": "bun run build"
16
+ },
17
+ "keywords": ["cinnabun", "scheduler", "cron", "scheduled"],
18
+ "peerDependencies": {
19
+ "@cinnabun/core": "^0.0.3",
20
+ "node-cron": ">=3.0.0"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "node-cron": {
24
+ "optional": true
25
+ }
26
+ },
27
+ "devDependencies": {
28
+ "@cinnabun/core": "workspace:*",
29
+ "@types/bun": "latest",
30
+ "node-cron": "^3.0.3",
31
+ "reflect-metadata": "^0.2.2",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }