From a1180813fc50d8dd534632bc99744b9a9a715a3f Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sun, 14 Apr 2024 18:20:29 +0100 Subject: [PATCH] feat: first commit --- .changeset/README.md | 8 + .changeset/config.json | 11 ++ .github/workflows/ci.yml | 26 +++ .github/workflows/release.yml | 41 +++++ .gitignore | 173 +++++++++++++++++++ README.md | 160 +++++++++++++++++ bun.lockb | Bin 0 -> 141040 bytes index.ts | 23 +++ lib/baker.ts | 103 +++++++++++ lib/cron.ts | 167 ++++++++++++++++++ lib/index.test.ts | 214 +++++++++++++++++++++++ lib/index.ts | 25 +++ lib/parser.ts | 266 ++++++++++++++++++++++++++++ lib/types.ts | 316 ++++++++++++++++++++++++++++++++++ lib/utils.ts | 12 ++ package.json | 68 ++++++++ tsconfig.json | 24 +++ 17 files changed, 1637 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 index.ts create mode 100644 lib/baker.ts create mode 100644 lib/cron.ts create mode 100644 lib/index.test.ts create mode 100644 lib/index.ts create mode 100644 lib/parser.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..91b6a95 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..59f9c10 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: ['main'] + pull_request: + types: [opened, synchronize] + +jobs: + test: + name: Test Bun + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + echo "$HOME/.bun/bin" >> $GITHUB_PATH + + - name: Install Dependencies + run: bun install + + - name: Run Test + run: bun test \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8bca556 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: + ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Install Bun + run: | + curl -fsSL https://bun.sh/install | bash + echo "$HOME/.bun/bin" >> $GITHUB_PATH + + - name: Install Dependencies + run: bun install + + - name: Test Package + run: bun test + + - name: Create Release PR or Publish Packages + id: changesets + uses: changesets/action@v1 + with: + publish: bun run release + version: bun run version + commit: 'chore: update package versions' + title: 'chore: update package versions' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..696107b --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +node_modules +dist +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd24edb --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# Cronbake + +Cronbake is a powerful and flexible cron job manager built with TypeScript. It provides an easy-to-use interface for scheduling and managing cron jobs with a wide range of options and features. Cronbake is designed to be lightweight, efficient, and highly configurable, making it suitable for a variety of use cases. + +## Features + +### Expressive Cron Expressions + +Cronbake supports a wide range of cron expressions, including standard formats, ranges, steps, lists, and presets. You can use the following formats or presets: + +- **Wildcards**: `* * * * * *` (second minute hour day month day-of-week) +- **Ranges**: `1-10 * * * * *` +- **Steps**: `1-10/2 * * * * *` (can be used with wildcards and ranges) +- **Lists**: `1,2,3 * * * * *` +- **Presets**: + - `@every_second` + - `@every_minute` + - `@yearly` or `@annually` + - `@monthly` + - `@weekly` + - `@daily` + - `@hourly` +- **Custom Presets**: + - `@every__` (where `` is one of `seconds`, `minutes`, `hours`, `dayOfMonth`, `months`, `dayOfWeek`) + - `@at_:` (where `` is a number between 0 and 23, and `` is a number between 0 and 59) + - `@on_` (where `` is one of `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`) + - `@between__` (where `` is a number between 0 and 23) + +### Cron Job Management + +Cronbake provides a simple and intuitive interface for managing cron jobs. You can easily add, remove, start, stop, and destroy cron jobs using the `Baker` class. + +### Real-time Status + +Cronbake allows you to get the current status, last execution time, next execution time, and remaining time for each cron job. This information can be useful for monitoring and debugging purposes. + +### Callbacks + +With Cronbake, you can execute custom functions when a cron job ticks (runs) or completes. This allows you to perform any necessary actions or side effects related to your cron job. + +### Type-safe + +Cronbake is built with TypeScript, ensuring type safety and better tooling support. This helps catch errors during development and provides better code navigation and auto-completion. + +## Installation + +You can install Cronbake using your preferred package manager: + +```bash +# With Bun +bun add cronbake + +# With npm +npm install cronbake + +# With yarn +yarn add cronbake + +# With pnpm +pnpm add cronbake +``` + +## Usage + +To get started with Cronbake, create a new instance of the `Baker` class and add cron jobs using the `add` method: + +```typescript +import Baker from 'cronbake'; + +// Create a new Baker instance +const baker = Baker.create(); + +// Add a cron job that runs daily at midnight +const dailyJob = baker.add({ + name: 'daily-job', + cron: '0 0 * * * *', // Runs daily at midnight + callback: () => { + console.log('Daily job executed!'); + }, +}); + +// Add a cron job that runs every 5 minutes +const everyFiveMinutes = baker.add({ + name: 'every-five-minutes', + cron: '*/5 * * * * *', // Runs every 5 minutes + callback: () => { + console.log('Job executed every 5 minutes!'); + }, +}); + +// Start all cron jobs +baker.bakeAll(); +``` + +You can manage cron jobs using the various methods provided by the `Baker` class, such as `remove`, `stop`, `destroy`, `getStatus`, `isRunning`, `lastExecution`, `nextExecution`, `remaining`, and `time`. + +### Advanced Usage + +Cronbake also provides a `Cron` class that you can use directly to create and manage individual cron jobs. This can be useful if you need more granular control over cron job instances. + +```typescript +import Cron from 'cronbake'; + +// Create a new Cron instance +const job = Cron.create({ + name: 'custom-job', + cron: '0 0 * * 0 *', // Runs every Sunday at midnight + callback: () => { + console.log('Custom job executed!'); + }, + onTick: () => { + console.log('Job ticked!'); + }, + onComplete: () => { + console.log('Job completed!'); + }, +}); + +// Start the cron job +job.start(); + +// Stop the cron job +job.stop(); + +// Get the job status +const status = job.getStatus(); + +// Get the next execution time +const nextExecution = job.nextExecution(); + +// ... +``` + +Cronbake also provides utility functions for parsing cron expressions, getting the next or previous execution times, and validating cron expressions. + +```typescript +import Cron from 'cronbake'; + +// Parse a cron expression +const cronTime = Cron.parse('0 0 * * 0 *'); + +// Get the next execution time for a cron expression +const nextExecution = Cron.getNext('0 0 * * 0 *'); + +// Get the previous execution time for a cron expression +const previousExecution = Cron.getPrevious('0 0 * * 0 *'); + +// Check if a string is a valid cron expression +const isValid = Cron.isValid('0 0 * * 0 *'); // true +``` + +For more advanced usage and examples, please refer to the [Documentation](link-to-documentation). + +## Contributing + +Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. + +## License + +Cronbake is released under the [MIT License](link-to-license). \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..637a56657814bdbe55449232dd7fa62c9e91f476 GIT binary patch literal 141040 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 = new Map(); + + constructor(options: IBakerOptions = {}) { + if (options.autoStart) { + this.bakeAll(); + } + } + + add(options: CronOptions): ICron { + const cron = Cron.create(options); + this.crons.set(cron.name, cron); + return cron; + } + + remove(name: string): void { + const cron = this.crons.get(name); + if (cron) { + cron.destroy(); + this.crons.delete(name); + } + } + + bake(name: string): void { + const cron = this.crons.get(name); + if (cron) { + cron.start(); + } + } + + stop(name: string): void { + const cron = this.crons.get(name); + if (cron) { + cron.stop(); + } + } + + destroy(name: string): void { + const cron = this.crons.get(name); + if (cron) { + cron.destroy(); + this.crons.delete(name); + } + } + + getStatus(name: string): Status { + const cron = this.crons.get(name); + return cron ? cron.getStatus() : 'stopped'; + } + + isRunning(name: string): boolean { + const cron = this.crons.get(name); + return cron ? cron.isRunning() : false; + } + + lastExecution(name: string): Date { + const cron = this.crons.get(name); + return cron ? cron.lastExecution() : new Date(); + } + + nextExecution(name: string): Date { + const cron = this.crons.get(name); + return cron ? cron.nextExecution() : new Date(); + } + + remaining(name: string): number { + const cron = this.crons.get(name); + return cron ? cron.remaining() : 0; + } + + time(name: string): number { + const cron = this.crons.get(name); + return cron ? cron.time() : 0; + } + + bakeAll(): void { + this.crons.forEach((cron) => cron.start()); + } + + stopAll(): void { + this.crons.forEach((cron) => cron.stop()); + } + + destroyAll(): void { + this.crons.forEach((cron) => cron.destroy()); + this.crons.clear(); + } + + /** + * Creates a new instance of `Baker`. + */ + public static create(options: IBakerOptions = {}) { + return new Baker(options); + } +} + +export default Baker; diff --git a/lib/cron.ts b/lib/cron.ts new file mode 100644 index 0000000..9b76add --- /dev/null +++ b/lib/cron.ts @@ -0,0 +1,167 @@ +import { + type CronExpressionType, + type CronOptions, + type CronTime, + type ICron, + type ICronParser, + type Status, +} from '@/lib/types'; +import { CronParser } from '@/lib'; +import { CBResolver } from '@/lib/utils'; + +/** + * A class that implements the `ICron` interface and provides methods manage a cron job. + */ +class Cron implements ICron { + name: string; + cron: CronExpressionType; + callback: () => void; + onTick: () => void; + onComplete: () => void; + private interval: Timer | null = null; + private next: Date | null = null; + private status: Status = 'stopped'; + private parser: ICronParser; + + /** + * Creates a new instance of the `Cron` class. + */ + constructor(options: CronOptions) { + this.name = options.name; + this.cron = options.cron; + this.callback = CBResolver.bind(this, options.callback); + this.onTick = CBResolver.bind(this, options.onTick); + this.onComplete = CBResolver.bind(this, options.onComplete); + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + this.destroy = this.destroy.bind(this); + this.getStatus = this.getStatus.bind(this); + this.isRunning = this.isRunning.bind(this); + this.lastExecution = this.lastExecution.bind(this); + this.nextExecution = this.nextExecution.bind(this); + this.remaining = this.remaining.bind(this); + this.time = this.time.bind(this); + this.parser = new CronParser(this.cron); + if (options.start) { + this.start(); + } + } + + start(): void { + if (this.status === 'running') { + return; + } + this.status = 'running'; + this.next = this.parser.getNext(); + this.interval = setInterval(() => { + if (this.next && this.next.getTime() <= Date.now()) { + this.callback(); + this.onTick(); + this.next = this.parser.getNext(); + } + }, 1000); + } + + stop(): void { + if (this.status === 'stopped') { + return; + } + this.status = 'stopped'; + if (this.interval) { + clearInterval(this.interval); + } + } + + destroy(): void { + this.stop(); + this.onComplete(); + } + + getStatus(): Status { + return this.status; + } + + isRunning(): boolean { + return this.status === 'running'; + } + + lastExecution(): Date { + return this.parser.getPrevious(); + } + + nextExecution(): Date { + return this.next || new Date(); + } + + remaining(): number { + return this.next ? this.next.getTime() - Date.now() : 0; + } + + time(): number { + return Date.now(); + } + + /** + * Creates a new cron job with the specified options. + * @returns A new `ICron` object representing the cron job. + */ + static create(options: CronOptions): ICron { + return new Cron(options); + } + + /** + * Parses the specified cron expression and returns a `CronTime` object. + * @returns A `CronTime` object representing the parsed cron expression. + */ + static parse( + cron: CronExpressionType, + ): CronTime { + return new CronParser(cron).parse(); + } + + /** + * Gets the next execution time for the specified cron expression. + * @template T The type of the cron expression. + * @returns A `Date` object representing the next execution time. + */ + static getNext(cron: CronExpressionType): Date { + return new CronParser(cron).getNext(); + } + + /** + * Gets the previous execution time for the specified cron expression. + * @returns A `Date` object representing the previous execution time. + */ + static getPrevious( + cron: CronExpressionType, + ): Date { + return new CronParser(cron).getPrevious(); + } + + /** + * Checks if the specified string is a valid cron expression. + * @returns `true` if the string is a valid cron expression, `false` otherwise. + */ + static isCron( + cron: CronExpressionType, + ): boolean { + try { + new CronParser(cron).parse(); + return true; + } catch (e) { + return false; + } + } + + /** + * Checks if the specified string is a valid cron expression. + * @returns `true` if the string is a valid cron expression, `false` otherwise. + */ + static isValid( + cron: CronExpressionType, + ): boolean { + return Cron.isCron(cron); + } +} + +export default Cron; diff --git a/lib/index.test.ts b/lib/index.test.ts new file mode 100644 index 0000000..31fe026 --- /dev/null +++ b/lib/index.test.ts @@ -0,0 +1,214 @@ +import { Baker, Cron, CronParser } from '@/dist'; +import { + expect, + describe, + it, + afterEach, + beforeEach, + jest, + Mock, +} from 'bun:test'; + +describe('Baker', () => { + let baker: Baker; + beforeEach(() => { + baker = new Baker(); + }); + + afterEach(() => { + baker.destroyAll(); + }); + + it('Should check all the presets', () => { + const presets = ['@daily', '@hourly', '@monthly', '@weekly', '@yearly']; + presets.forEach((preset) => { + const parser = new CronParser(preset); + const cronTime = parser.parse(); + expect(cronTime).toBeDefined(); + }); + }); + + it('should add a cron job', () => { + const cron = baker.add({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + expect(cron).toBeDefined(); + }); + + it('should remove a cron job', () => { + baker.add({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.remove('test'); + expect(baker.isRunning('test')).toBeFalsy(); + }); + + it('should start a cron job', () => { + baker.add({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.bake('test'); + expect(baker.isRunning('test')).toBeTruthy(); + }); + + it('should stop a cron job', () => { + baker.add({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.bake('test'); + baker.stop('test'); + expect(baker.isRunning('test')).toBeFalsy(); + }); + + it('should destroy a cron job', () => { + baker.add({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.destroy('test'); + expect(baker.isRunning('test')).toBeFalsy(); + }); + + it('should start all cron jobs', () => { + baker.add({ + name: 'test1', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.add({ + name: 'test2', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.bakeAll(); + expect(baker.isRunning('test1')).toBeTruthy(); + expect(baker.isRunning('test2')).toBeTruthy(); + }); + + it('should stop all cron jobs', () => { + baker.add({ + name: 'test1', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.add({ + name: 'test2', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.bakeAll(); + baker.stopAll(); + expect(baker.isRunning('test1')).toBeFalsy(); + expect(baker.isRunning('test2')).toBeFalsy(); + }); + + it('should destroy all cron jobs', () => { + baker.add({ + name: 'test1', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.add({ + name: 'test2', + cron: '* * * * * *', + callback: jest.fn(), + }); + baker.destroyAll(); + expect(baker.isRunning('test1')).toBeFalsy(); + expect(baker.isRunning('test2')).toBeFalsy(); + }); +}); + +describe('CronParser', () => { + let parser: CronParser; + + beforeEach(() => { + parser = new CronParser('* * * * * *'); + }); + + it('should parse the cron expression', () => { + const cronTime = parser.parse(); + expect(cronTime).toBeDefined(); + }); + + it('should get the next execution time', () => { + const nextExecution = parser.getNext(); + expect(nextExecution).toBeInstanceOf(Date); + }); + + it('should get the previous execution time', () => { + const previousExecution = parser.getPrevious(); + expect(previousExecution).toBeInstanceOf(Date); + }); +}); + +describe('Cron', () => { + let cron: Cron; + + beforeEach(() => { + cron = new Cron({ + name: 'test', + cron: '* * * * * *', + callback: jest.fn(), + }); + }); + + afterEach(() => { + cron.destroy(); + }); + + it('should start the cron job', () => { + cron.start(); + expect(cron.isRunning()).toBeTruthy(); + }); + + it('should stop the cron job', () => { + cron.start(); + cron.stop(); + expect(cron.isRunning()).toBeFalsy(); + }); + + it('should destroy the cron job', () => { + cron.destroy(); + expect(cron.isRunning()).toBeFalsy(); + }); + + it('should get the status of the cron job', () => { + const status = cron.getStatus(); + expect(status).toBeDefined(); + }); + + it('should check if the cron job is running', () => { + const isRunning = cron.isRunning(); + expect(isRunning).toBeFalsy(); + }); + + it('should get the date of the last execution of the cron job', () => { + const lastExecution = cron.lastExecution(); + expect(lastExecution).toBeInstanceOf(Date); + }); + + it('should get the date of the next execution of the cron job', () => { + const nextExecution = cron.nextExecution(); + expect(nextExecution).toBeInstanceOf(Date); + }); + + it('should get the remaining time until the next execution of the cron job', () => { + const remaining = cron.remaining(); + expect(remaining).toBeDefined(); + }); + + it('should get the time until the next execution of the cron job', () => { + const time = cron.time(); + expect(time).toBeDefined(); + }); +}); diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..c50f6dc --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,25 @@ +import Cron from "@/lib/cron"; +import Baker from "@/lib/baker"; +import CronParser from "@/lib/parser"; + +export { + type CronOptions, + type CronTime, + type ICron, + type IBaker, + type IBakerOptions, + type ICronParser, + type Status, + type AtHourStrType, + type BetweenStrType, + type CronExpression, + type CronExpressionType, + type CronExprs, + type EveryStrType, + type OnDayStrType, + type day, + type unit, +} from "@/lib/types"; + +export { Cron, Baker, CronParser }; +export default Baker; diff --git a/lib/parser.ts b/lib/parser.ts new file mode 100644 index 0000000..bacab86 --- /dev/null +++ b/lib/parser.ts @@ -0,0 +1,266 @@ +import { + type AtHourStrType, + type BetweenStrType, + type CronExpression, + type CronExpressionType, + type CronTime, + type EveryStrType, + type ICronParser, + type OnDayStrType, +} from "@/lib/types"; + +/** + * A class that implements the `ICronParser` interface and provides methods to parse a cron expression + * and get the next and previous execution times. + */ +class CronParser implements ICronParser { + /** + * Creates a new instance of the `CronParser` class. + */ + constructor(private cron: CronExpressionType) {} + + /** + * A map of cron expression aliases to their corresponding cron expressions. + */ + private readonly aliases: Map = new Map([ + ["@every_second", "* * * * * *"], + ["@every_minute", "0 * * * * *"], + ["@yearly", "0 0 1 1 * *"], + ["@annually", "0 0 1 1 * *"], + ["@monthly", "0 0 1 * * *"], + ["@weekly", "0 0 * * 0 *"], + ["@daily", "0 0 * * * *"], + ["@hourly", "0 * * * * *"], + ]); + + /** + * Parses a string in the format "@every__" and returns the corresponding cron expression. + */ + private parseEveryStr(str: EveryStrType): string { + const [, value, unit] = str.split("_"); + switch (unit) { + case "seconds": + return `*/${value} * * * * *`; + case "minutes": + return `0 */${value} * * * *`; + case "hours": + return `0 0 */${value} * * *`; + case "dayOfMonth": + return `0 0 0 */${value} * *`; + case "months": + return `0 0 0 0 */${value} *`; + case "dayOfWeek": + return `0 0 0 0 0 */${value}`; + default: + return "* * * * * *"; + } + } + + /** + * Parses a string in the format "@at_