[
  {
    "path": ".commitlintrc",
    "content": "{\n\t\"extends\": [\n\t\t\"@insurgent/commitlint-config\"\n\t]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*.{js}]\nindent_style = tab\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yaml",
    "content": "name: Bug\ndescription: Report a bug\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Provide a more detailed introduction to the issue itself, and why you consider it to be a bug\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: Tell us what should happen\n    validations:\n      required: true\n  - type: textarea\n    id: actual-behavior\n    attributes:\n      label: Actual Behavior\n      description: Tell us what happens instead\n    validations:\n      required: true\n  - type: textarea\n    id: possible-fix\n    attributes:\n      label: Possible Fix\n      description: Not obligatory, but suggest a fix or reason for the bug\n  - type: textarea\n    id: reproducing\n    attributes:\n      label: Steps to Reproduce\n      description: Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Context\n      description: How has this bug affected you? What were you trying to accomplish?\n    validations:\n      required: true\n  - type: textarea\n    id: environment\n    attributes:\n      label: Your Environment\n      description: Include as many relevant details about the environment you experienced the bug in\n      value: '- **`cron` version**:\n\n        - **NodeJS version**:\n\n        - **Operating System and version**:\n\n        - **TypeScript version (if applicable)**:\n\n        - **Link to your project (if applicable)**:'\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yaml",
    "content": "name: Feature/improvement request\ndescription: Suggest new features or improvements\nlabels: ['type:feature']\nbody:\n  - type: textarea\n    id: suggestion\n    attributes:\n      label: ⭐ Suggestion\n      description: A summary of what you'd like to see added or changed\n    validations:\n      required: true\n  - type: textarea\n    id: usecases\n    attributes:\n      label: 💻 Use Cases\n      description: |\n        What are possible test cases for your suggested feature?\n        Are you using any workarounds in the meantime?\n    validations:\n      required: false\n  - type: textarea\n    id: relatedproblems\n    attributes:\n      label: ❌ Related Problems\n      description: |\n        Is your Request related to a problem?\n        Think about linking existing Issues here!\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Provide a general summary of your changes in the Title above (following the Conventional Commits standard) -->\n<!-- More infos: https://www.conventionalcommits.org -->\n<!-- Commit types: https://github.com/insurgent-lab/conventional-changelog-preset#commit-types-->\n\n## Description\n\n<!--- Describe your changes in detail -->\n\n## Related Issue\n\n<!--- This project only accepts pull requests related to open issues -->\n<!--- If suggesting a new feature or change, please discuss it in an issue first -->\n<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->\n<!--- Please link to the issue here: -->\n\n## Motivation and Context\n\n<!--- Why is this change required? What problem does it solve? -->\n\n## How Has This Been Tested?\n\n<!--- Please describe in detail how you tested your changes. -->\n<!--- Include details of your testing environment, and the tests you ran to -->\n<!--- see how your change affects other areas of the code, etc. -->\n\n## Screenshots (if appropriate):\n\n## Types of changes\n\n<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->\n\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to change)\n\n## Checklist:\n\n<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->\n<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->\n\n- [ ] My code follows the code style of this project.\n- [ ] My change requires a change to the documentation.\n- [ ] I have updated the documentation accordingly.\n- [ ] I have added tests to cover my changes.\n- [ ] All new and existing tests passed.\n- [ ] If my change introduces a breaking change, I have added a `!` after the type/scope in the title (see the Conventional Commits standard).\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\n**Please do not report security vulnerabilities through public GitHub issues.**\n\nInstead, please report it in a private conversation with one or more of our maintainers [on Discord](https://discord.gg/yyKns29zch).\n\nPlease encrypt your message to us using our PGP key. The key fingerprint is:\n\n```\nA656 0650 74D2 6C7D CF6E D0F4 0784 3C69 92BF C9FA\n```\n\nThe key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0xA656065074D26C7DCF6ED0F407843C6992BFC9FA&fingerprint=on&op=index).\n\nPlease include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:\n\n- Full paths of source file(s) related to the manifestation of the issue\n- The location of the affected source code (tag/branch/commit or direct URL)\n- Any special configuration required to reproduce the issue\n- Step-by-step instructions to reproduce the issue\n- Proof-of-concept or exploit code (if possible)\n- Impact of the issue, including how an attacker might exploit the issue\n\nPlease get in touch and give the project contributors a chance to resolve the vulnerability and issue a new release prior to any public exposure; this helps protect the project's users and provides them with a chance to upgrade and/or update in order to protect their applications.\n\n## Preferred Languages\n\nWe prefer all communications to be in English.\n\n## Policy\n\n`cron` follows the principle of [Coordinated Vulnerability Disclosure](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#responsible-or-coordinated-disclosure).\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n\t\"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n\t\"extends\": [\n\t\t\"config:recommended\",\n\t\t\":dependencyDashboard\",\n\t\t\":disableRateLimiting\",\n\t\t\":pinOnlyDevDependencies\",\n\t\t\"npm:unpublishSafe\",\n\t\t\"docker:pinDigests\",\n\t\t\"helpers:pinGitHubActionDigestsToSemver\",\n\t\t\"security:openssf-scorecard\"\n\t],\n\t\"ignorePresets\": [\n\t\t\":semanticPrefixFixDepsChoreOthers\",\n\t\t\"group:semantic-releaseMonorepo\",\n\t\t\"group:commitlintMonorepo\"\n\t],\n\t\"schedule\": [\"before 5am every weekday\", \"every weekend\"],\n\t\"lockFileMaintenance\": {\n\t\t\"enabled\": true,\n\t\t\"automerge\": true,\n\t\t\"automergeType\": \"branch\"\n\t},\n\t\"labels\": [\"dependencies\"],\n\t\"osvVulnerabilityAlerts\": true,\n\t\"packageRules\": [\n\t\t{\n\t\t\t\"matchPackageNames\": [\"*\"],\n\t\t\t\"semanticCommitType\": \"chore\",\n\t\t\t\"semanticCommitScope\": \"deps\",\n\t\t\t\"schedule\": \"* * 10,25 * *\"\n\t\t},\n\t\t{\n\t\t\t\"matchDepTypes\": [\"devDependencies\"],\n\t\t\t\"matchUpdateTypes\": [\"minor\", \"patch\", \"pin\", \"pinDigest\"],\n\t\t\t\"automerge\": true,\n\t\t\t\"automergeType\": \"branch\"\n\t\t},\n\t\t{\n\t\t\t\"matchDepTypes\": [\"dependencies\"],\n\t\t\t\"semanticCommitType\": \"build\",\n\t\t\t\"semanticCommitScope\": \"deps\",\n\t\t\t\"schedule\": \"at any time\"\n\t\t},\n\t\t{\n\t\t\t\"matchDepTypes\": [\"dependencies\"],\n\t\t\t\"matchUpdateTypes\": [\"minor\", \"patch\"],\n\t\t\t\"semanticCommitType\": \"build\",\n\t\t\t\"automerge\": true,\n\t\t\t\"automergeType\": \"branch\"\n\t\t},\n\t\t{\n\t\t\t\"matchManagers\": [\"github-actions\"],\n\t\t\t\"semanticCommitType\": \"chore\",\n\t\t\t\"semanticCommitScope\": \"action\",\n\t\t\t\"schedule\": \"* * 10,25 * *\"\n\t\t},\n\t\t{\n\t\t\t\"matchManagers\": [\"github-actions\"],\n\t\t\t\"matchUpdateTypes\": [\"minor\", \"patch\", \"pin\", \"pinDigest\"],\n\t\t\t\"automerge\": true,\n\t\t\t\"automergeType\": \"branch\"\n\t\t},\n\t\t{\n\t\t\t\"extends\": [\"monorepo:semantic-release\"],\n\t\t\t\"groupName\": \"semantic-release related packages\",\n\t\t\t\"matchUpdateTypes\": [\"digest\", \"patch\", \"minor\", \"major\"]\n\t\t},\n\t\t{\n\t\t\t\"extends\": [\"monorepo:commitlint\"],\n\t\t\t\"groupName\": \"semantic-release related packages\",\n\t\t\t\"matchUpdateTypes\": [\"digest\", \"patch\", \"minor\", \"major\"]\n\t\t},\n\t\t{\n\t\t\t\"groupName\": \"semantic-release related packages\",\n\t\t\t\"matchUpdateTypes\": [\"digest\", \"patch\", \"minor\", \"major\"],\n\t\t\t\"matchPackageNames\": [\n\t\t\t\t\"/@insurgent/conventional-changelog-preset/\",\n\t\t\t\t\"/@insurgent/commitlint-config/\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"extends\": [\"packages:linters\"],\n\t\t\t\"groupName\": \"linters\",\n\t\t\t\"addLabels\": [\"linters\"]\n\t\t},\n\t\t{\n\t\t\t\"extends\": [\"packages:test\"],\n\t\t\t\"groupName\": \"tests\",\n\t\t\t\"addLabels\": [\"tests\"]\n\t\t},\n\t\t{\n\t\t\t\"matchDepTypes\": [\"devDependencies\"],\n\t\t\t\"matchUpdateTypes\": [\"minor\", \"patch\"],\n\t\t\t\"automerge\": true,\n\t\t\t\"automergeType\": \"branch\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: 'CodeQL'\n\non:\n  push:\n    branches: ['main']\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: ['main']\n  schedule:\n    - cron: '0 0 * * 1'\n\npermissions:\n  contents: read\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: ['typescript']\n        # CodeQL supports [ $supported-codeql-languages ]\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1\n        with:\n          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs\n\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n      # If this step fails, then you should remove it and run the build manually (see below)\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n\n      # ℹ️ Command-line programs to run using the OS shell.\n      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n      #   If the Autobuild fails above, remove it and uncomment the following three lines.\n      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n      # - run: |\n      #   echo \"Run, Build Application using script\"\n      #   ./location_of_script_within_repo/buildscript.sh\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n        with:\n          category: '/language:${{matrix.language}}'\n"
  },
  {
    "path": ".github/workflows/lint_pr_title.yml",
    "content": "name: 'Lint PR title'\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\npermissions:\n  pull-requests: write\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1\n        with:\n          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs\n\n      - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1\n        id: lint_pr_title\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          HUSKY: 0\n\n      - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4\n        # When the previous steps fails, the workflow would stop. By adding this\n        # condition you can continue the execution with the populated error message.\n        if: always() && (steps.lint_pr_title.outputs.error_message != null)\n        with:\n          header: pr-title-lint-error\n          message: |\n            Hey there and thank you for opening this pull request! 👋🏼\n\n            We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.\n\n            Details:\n\n            ```\n            ${{ steps.lint_pr_title.outputs.error_message }}\n            ```\n\n      # Delete a previous comment when the issue has been resolved\n      - if: ${{ steps.lint_pr_title.outputs.error_message == null }}\n        uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4\n        with:\n          header: pr-title-lint-error\n          delete: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - main\n      - beta\n      - '+([0-9])?(.{+([0-9]),x}).x'\n\njobs:\n  test:\n    uses: ./.github/workflows/test.yml\n\n  release:\n    needs: test\n\n    runs-on: ubuntu-latest\n\n    # using trusted publishing. see https://docs.npmjs.com/trusted-publishers\n    permissions:\n      id-token: write\n      contents: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1\n        with:\n          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs\n\n      - name: Generate token\n        id: get_workflow_token\n        uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n        with:\n          app-id: ${{ secrets.NODE_CRON_RELEASE_APP_ID }}\n          private-key: ${{ secrets.NODE_CRON_RELEASE_APP_PRIVATE_KEY }}\n\n      - name: Checkout project\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          token: ${{ steps.get_workflow_token.outputs.token }}\n\n      - name: Use Node.js LTS\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: 'lts/*'\n          cache: npm\n\n      - name: Install packages\n        run: npm ci\n\n      - name: Audit npm signatures\n        run: npm audit signatures\n\n      - name: Build project\n        run: npm run build\n\n      - name: Run Semantic Release\n        run: npx semantic-release\n        env:\n          GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}\n          HUSKY: 0\n"
  },
  {
    "path": ".github/workflows/scorecards.yml",
    "content": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by separate terms of service, privacy\n# policy, and support documentation.\n\nname: Scorecard supply-chain security\non:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n    - cron: '20 7 * * 2'\n  push:\n    branches: ['main']\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n      contents: read\n      actions: read\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1\n        with:\n          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs\n\n      - name: 'Checkout code'\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: 'Run analysis'\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n          # - you want to enable the Branch-Protection check on a *public* repository, or\n          # - you are installing Scorecards on a *private* repository\n          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.\n          # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: 'Upload artifact'\n        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: 'Upload to code-scanning'\n        uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - renovate/** # branches generated by https://github.com/apps/renovate\n  pull_request:\n    branches:\n      - main\n      - beta\n      - '+([0-9])?(.{+([0-9]),x}).x'\n  workflow_call:\n\npermissions:\n  contents: read\n\njobs:\n  # prevent duplicate checks on Renovate PRs\n  prevent-duplicate-checks:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: insurgent-lab/is-in-pr-action@129df59687402c4a9c81a9a9e88d7448cdbba541 # v0.2.0\n        id: isInPR\n    outputs:\n      should-run: ${{ !(steps.isInPR.outputs.result == 'true' && startsWith(github.ref, 'refs/heads/renovate/')) }}\n\n  test_matrix:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        node: [18, 20, 22, 23, 24, 25]\n\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 5\n\n    needs: prevent-duplicate-checks\n    if: ${{ needs.prevent-duplicate-checks.outputs.should-run == 'true' }}\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1\n        with:\n          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs\n\n      - name: Checkout project\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Use Node.js ${{ matrix.node }}\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: ${{ matrix.node }}\n          cache: 'npm'\n\n      - name: Install packages\n        run: npm ci\n\n      - name: Audit npm signatures\n        run: npm audit signatures\n\n      - name: Check codestyle compliance\n        run: npm run lint\n\n      - name: Build project\n        run: npm run build\n\n      - name: Run tests\n        run: npm run test\n\n      - name: Run fuzz tests\n        run: npm run test:fuzz\n\n  # separate job to set as required status check in branch protection\n  required_check:\n    runs-on: ubuntu-latest\n    needs:\n      - prevent-duplicate-checks\n      - test_matrix\n\n    if: ${{ !cancelled() && needs.prevent-duplicate-checks.outputs.should-run == 'true' }}\n    steps:\n      - name: All required jobs and matrix versions passed\n        if: ${{ !(contains(needs.*.result, 'failure')) }}\n        run: exit 0\n      - name: Some required jobs or matrix versions failed\n        if: ${{ contains(needs.*.result, 'failure') }}\n        run: exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# husky local debugging helper scripts\n.husky/_\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "# only run commitlint on main (for admins pushing directly to branch)\n[ \"$(git rev-parse --abbrev-ref HEAD)\" != \"main\" ] && exit 0\n\nnpx --no -- commitlint --edit ${1}\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx lint-staged\n"
  },
  {
    "path": ".nvmrc",
    "content": "24.12.0\n"
  },
  {
    "path": ".prettierignore",
    "content": "CHANGELOG.md\ndist/\ncoverage/\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n\t\"arrowParens\": \"avoid\",\n\t\"endOfLine\": \"auto\",\n\t\"singleQuote\": true,\n\t\"trailingComma\": \"none\",\n\t\"useTabs\": true\n}\n"
  },
  {
    "path": ".releaserc",
    "content": "{\n\t\"repositoryUrl\": \"git@github.com:kelektiv/node-cron.git\",\n\t\"branches\": [\n\t\t\"main\",\n\t\t{\n\t\t\t\"name\": \"beta\",\n\t\t\t\"prerelease\": true\n\t\t},\n\t\t\"+([0-9])?(.{+([0-9]),x}).x\"\n\t],\n\t\"tagFormat\": \"v${version}\",\n\t\"plugins\": [\n\t\t[\n\t\t\t\"@semantic-release/commit-analyzer\",\n\t\t\t{\n\t\t\t\t\"config\": \"@insurgent/conventional-changelog-preset\",\n\t\t\t\t\"releaseRules\": \"@insurgent/conventional-changelog-preset/release-rules\"\n\t\t\t}\n\t\t],\n\t\t[\n\t\t\t\"@semantic-release/release-notes-generator\",\n\t\t\t{\n\t\t\t\t\"config\": \"@insurgent/conventional-changelog-preset\"\n\t\t\t}\n\t\t],\n        [\n            \"@semantic-release/npm\",\n            {\n                \"npmPublish\": true,\n                \"provenance\": true\n            }\n        ],\n\t\t[\n\t\t\t\"@semantic-release/changelog\",\n\t\t\t{\n\t\t\t\t\"changelogFile\": \"CHANGELOG.md\"\n\t\t\t}\n\t\t],\n\t\t[\n\t\t\t\"@semantic-release/git\",\n\t\t\t{\n\t\t\t\t\"assets\": [\n\t\t\t\t\t\"CHANGELOG.md\",\n\t\t\t\t\t\"package.json\",\n\t\t\t\t\t\"package-lock.json\"\n\t\t\t\t],\n\t\t\t\t\"message\": \"Release v${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}\"\n\t\t\t}\n\t\t],\n\t\t\"@semantic-release/github\"\n\t]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n\t\"version\": \"1.0.0\",\n\t\"configurations\": [\n\t\t{\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"name\": \"Jest Debug\",\n\t\t\t\"env\": { \"NODE_ENV\": \"test\" },\n\t\t\t\"program\": \"${workspaceFolder}/node_modules/.bin/jest\",\n\t\t\t\"args\": [\"--runInBand\"],\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"windows\": {\n\t\t\t\t\"program\": \"${workspaceFolder}/node_modules/jest/bin/jest\"\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [4.4.0](https://github.com/kelektiv/node-cron/compare/v4.3.5...v4.4.0) (2025-12-09)\n\n### ✨ Features\n\n* update node versions to include 24 and 25 ([#1031](https://github.com/kelektiv/node-cron/issues/1031)) ([b228e7c](https://github.com/kelektiv/node-cron/commit/b228e7cdf92106fe1af614768b2492e1cefbc20f)), closes [#1000](https://github.com/kelektiv/node-cron/issues/1000)\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([570f9bc](https://github.com/kelektiv/node-cron/commit/570f9bc69bf729890c2a409a99a1eec1b287bc0a))\n* **deps:** lock file maintenance ([#1026](https://github.com/kelektiv/node-cron/issues/1026)) ([ac05b84](https://github.com/kelektiv/node-cron/commit/ac05b84a238a4564a890760bdac802d591296872))\n\n## [4.3.5](https://github.com/kelektiv/node-cron/compare/v4.3.4...v4.3.5) (2025-11-30)\n\n### 🐛 Bug Fixes\n\n* suppress setTimeout warning with negatives ([#1030](https://github.com/kelektiv/node-cron/issues/1030)) ([74d3b74](https://github.com/kelektiv/node-cron/commit/74d3b74dd42adeb348d6e13dd798d61fb68073a7)), closes [#1000](https://github.com/kelektiv/node-cron/issues/1000)\n\n### ♻️ Chores\n\n* **action:** update actions/checkout action to v5.0.1 ([0f3b9f3](https://github.com/kelektiv/node-cron/commit/0f3b9f3c12ea9e2cb8567570551f79d557f53b5f))\n* **action:** update actions/checkout action to v6 ([#1028](https://github.com/kelektiv/node-cron/issues/1028)) ([232f23a](https://github.com/kelektiv/node-cron/commit/232f23adad475b70f8c958b29d47f32eb905fb96))\n* **action:** update actions/create-github-app-token action to v2.2.0 ([1ade9ce](https://github.com/kelektiv/node-cron/commit/1ade9ce4ac91a5703798e1c219c59bf92abfdd81))\n* **action:** update actions/setup-node action to v6 ([#1017](https://github.com/kelektiv/node-cron/issues/1017)) ([288cf0d](https://github.com/kelektiv/node-cron/commit/288cf0d83f546b86d5a7bd804a8e88182a6f38cd))\n* **action:** update actions/upload-artifact action to v5 ([#1018](https://github.com/kelektiv/node-cron/issues/1018)) ([7091186](https://github.com/kelektiv/node-cron/commit/709118655aab2d222f50f3a9aff36bc50e0742fa))\n* **action:** update github/codeql-action action to v3.31.2 ([8c5c4db](https://github.com/kelektiv/node-cron/commit/8c5c4db54cc2221181f839c849766dbf54a886db))\n* **action:** update github/codeql-action action to v3.31.5 ([c6516f1](https://github.com/kelektiv/node-cron/commit/c6516f1cbfa8c0d3e453f5d77b14ac02faabc59a))\n* **action:** update github/codeql-action action to v4 ([#1014](https://github.com/kelektiv/node-cron/issues/1014)) ([258ee3b](https://github.com/kelektiv/node-cron/commit/258ee3b0502042720fef103f864e7ffea34783ac))\n* **action:** update step-security/harden-runner action to v2.13.2 ([2f44428](https://github.com/kelektiv/node-cron/commit/2f44428f3aec7829a049bb11cbbb2ed346fdf579))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.39.1 ([319462a](https://github.com/kelektiv/node-cron/commit/319462a0f972c3b1c15aa62c4b4bbfcdf7cfe251))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v12.0.1 ([dfa3411](https://github.com/kelektiv/node-cron/commit/dfa341118e126dbe2e2f7be36b81d9a434cc9608))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.15.0 ([7aa02a2](https://github.com/kelektiv/node-cron/commit/7aa02a2e5996902ecbe9ed69c047cf0093cbea71))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.15.3 ([af87e4d](https://github.com/kelektiv/node-cron/commit/af87e4d391315d72ddddaddac96e43c0eef1936e))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.19.0 ([67701aa](https://github.com/kelektiv/node-cron/commit/67701aa2de16a78927ed788c07158e0b2d34fe46))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.19.1 ([2c0d2bb](https://github.com/kelektiv/node-cron/commit/2c0d2bb069847f00ab0246499ea7e21fe34eed6f))\n* **deps:** update dependency [@types](https://github.com/types)/sinon to v21 ([#1029](https://github.com/kelektiv/node-cron/issues/1029)) ([028b8b7](https://github.com/kelektiv/node-cron/commit/028b8b737c1a03c4baba4ebb325e04221adf686c))\n* **deps:** update semantic-release related packages ([e29b122](https://github.com/kelektiv/node-cron/commit/e29b122f0c220b654bdfeeccf6a3408144efd145))\n\n## [4.3.4](https://github.com/kelektiv/node-cron/compare/v4.3.3...v4.3.4) (2025-11-06)\n\n### 🐛 Bug Fixes\n\n* catch errors in async onTick functions ([#1013](https://github.com/kelektiv/node-cron/issues/1013)) ([2ac3001](https://github.com/kelektiv/node-cron/commit/2ac300176017202cf74e281f0ec85541a961c5d2))\n\n### 🛠 Builds\n\n* add GitHub app token to use for release ([#1024](https://github.com/kelektiv/node-cron/issues/1024)) ([61b54f6](https://github.com/kelektiv/node-cron/commit/61b54f62940105312b4d5921a08ec57231474edb))\n* remove chai since we aren't using it ([#1012](https://github.com/kelektiv/node-cron/issues/1012)) ([cf14205](https://github.com/kelektiv/node-cron/commit/cf14205a36898cad43c4ad36460a9eb457882bdf))\n* switch to using built in GitHub token ([#1022](https://github.com/kelektiv/node-cron/issues/1022)) ([d24b3ea](https://github.com/kelektiv/node-cron/commit/d24b3ea1d19b463bf27826513d8f917d56aea77b))\n* update release config to use trusted publishing ([#1023](https://github.com/kelektiv/node-cron/issues/1023)) ([0cb3ff6](https://github.com/kelektiv/node-cron/commit/0cb3ff6e6a561aec4ea5e2e120eb78a31a5f9b5e)), closes [#1017](https://github.com/kelektiv/node-cron/issues/1017) [#1018](https://github.com/kelektiv/node-cron/issues/1018)\n* use trusted publishing to publish to NPM ([#1021](https://github.com/kelektiv/node-cron/issues/1021)) ([44f14f3](https://github.com/kelektiv/node-cron/commit/44f14f38dcbf405053354c4e1f5dbcad6d757b66))\n\n### ♻️ Chores\n\n* **action:** update actions/checkout action to v4.3.0 ([d8913b8](https://github.com/kelektiv/node-cron/commit/d8913b8a835e79095b84695baf3b6eea9a30bb43))\n* **action:** update actions/checkout action to v5 ([#1005](https://github.com/kelektiv/node-cron/issues/1005)) ([2e2a021](https://github.com/kelektiv/node-cron/commit/2e2a02198b3e74b6526499ce1e246ed9aba9d587))\n* **action:** update actions/setup-node action to v5 ([#1009](https://github.com/kelektiv/node-cron/issues/1009)) ([4a7f1f3](https://github.com/kelektiv/node-cron/commit/4a7f1f346d491632679183dbe1bfba4eabd48286))\n* **action:** update amannn/action-semantic-pull-request action to v6 ([#1006](https://github.com/kelektiv/node-cron/issues/1006)) ([832ca6e](https://github.com/kelektiv/node-cron/commit/832ca6e40687bd13153001c243f684173e140cf7))\n* **action:** update github/codeql-action action to v3.29.11 ([ec90183](https://github.com/kelektiv/node-cron/commit/ec90183c7be532d0dd45a97fd4aa165a16c81d55))\n* **action:** update github/codeql-action action to v3.29.8 ([842e3e0](https://github.com/kelektiv/node-cron/commit/842e3e0cfe1a581a0993705e09440dd952723c39))\n* **action:** update github/codeql-action action to v3.30.3 ([#1010](https://github.com/kelektiv/node-cron/issues/1010)) ([b195a01](https://github.com/kelektiv/node-cron/commit/b195a015471dca39b59220f745f2f840726ec9ef))\n* **action:** update github/codeql-action action to v3.30.4 ([45a48b8](https://github.com/kelektiv/node-cron/commit/45a48b8c9067e947b734ccbe8ab1de700f2d80de))\n* **action:** update github/codeql-action action to v3.30.7 ([5de5bfc](https://github.com/kelektiv/node-cron/commit/5de5bfcc31f7657acbfce9933ef5bb17ada72bcd))\n* **action:** update github/codeql-action action to v3.30.8 ([4df56a5](https://github.com/kelektiv/node-cron/commit/4df56a5acf9eb38a1aa47baafb729d2685b1b05b))\n* **action:** update github/codeql-action action to v3.31.0 ([14d7498](https://github.com/kelektiv/node-cron/commit/14d74984fcfd9bdcd5a04461582205b3f4e2f678))\n* **action:** update ossf/scorecard-action action to v2.4.3 ([e8a33a0](https://github.com/kelektiv/node-cron/commit/e8a33a05fd7c417583957296af5695dd8b751c65))\n* **action:** update step-security/harden-runner action to v2.13.1 ([2a4a2c2](https://github.com/kelektiv/node-cron/commit/2a4a2c2c94abdf6a2c6081fd844c5fffd09733bd))\n* **deps:** lock file maintenance ([1de94a3](https://github.com/kelektiv/node-cron/commit/1de94a3afcf130b608310a3e3613044f7c6d504b))\n* **deps:** lock file maintenance ([420c4b1](https://github.com/kelektiv/node-cron/commit/420c4b1b93eceb9afba7442f2bd77446df2506bf))\n* **deps:** lock file maintenance ([ac128a2](https://github.com/kelektiv/node-cron/commit/ac128a2d64a34c9d4d984ef6f2bdf2cab7a030e5))\n* **deps:** lock file maintenance ([573faca](https://github.com/kelektiv/node-cron/commit/573facaec3e62dcbb66140cb9240f448322ae275))\n* **deps:** lock file maintenance ([bbb3ab2](https://github.com/kelektiv/node-cron/commit/bbb3ab21bc6249ae8206679cba381dd91a2105bd))\n* **deps:** lock file maintenance ([fd06770](https://github.com/kelektiv/node-cron/commit/fd06770c325b07252682ddaa619948d379be37c4))\n* **deps:** lock file maintenance ([3c5769e](https://github.com/kelektiv/node-cron/commit/3c5769efeff562a436703f120f2c37b84f0c7f74))\n* **deps:** lock file maintenance ([51e2121](https://github.com/kelektiv/node-cron/commit/51e212188a1d6787a55c34d7aa1616e17e68d10d))\n* **deps:** lock file maintenance ([daf30a6](https://github.com/kelektiv/node-cron/commit/daf30a60c2ed3f9b1b70f0b40999f3204d9b5536))\n* **deps:** lock file maintenance ([a60f049](https://github.com/kelektiv/node-cron/commit/a60f04931e0de28d8a716f52b83011c6c9036ef2))\n* **deps:** lock file maintenance ([555cbbf](https://github.com/kelektiv/node-cron/commit/555cbbfe82a23fbb0d0dac5d6e8f9ebd61e70a4a))\n* **deps:** lock file maintenance ([a330852](https://github.com/kelektiv/node-cron/commit/a330852fce41c60c83abebc9b09506dae8486d11))\n* **deps:** lock file maintenance ([90fbb48](https://github.com/kelektiv/node-cron/commit/90fbb487bc84f8da0bbea61be0595c91e3b22fa6))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.34.0 ([cdf3a2d](https://github.com/kelektiv/node-cron/commit/cdf3a2de3a3040b2f9ef8427a462d09ceb8a420a))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.35.0 ([#1011](https://github.com/kelektiv/node-cron/issues/1011)) ([64c84bd](https://github.com/kelektiv/node-cron/commit/64c84bd04e2218652d43bfc0582f56323adedc22))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.36.0 ([23e0fbc](https://github.com/kelektiv/node-cron/commit/23e0fbc572aa8c33696766f16e7b75163fb046ae))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.37.0 ([3a20922](https://github.com/kelektiv/node-cron/commit/3a209225620cc013b06ece09445a4fbcea6b7627))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.38.0 ([#1020](https://github.com/kelektiv/node-cron/issues/1020)) ([b853a38](https://github.com/kelektiv/node-cron/commit/b853a388754a49f853e8b011e52eb96fd8267783))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v11.0.4 ([#1007](https://github.com/kelektiv/node-cron/issues/1007)) ([d04396d](https://github.com/kelektiv/node-cron/commit/d04396da14c3f8b17b4ca14e28a0470b8eaa3c34))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.13.20 ([#1019](https://github.com/kelektiv/node-cron/issues/1019)) ([c906a92](https://github.com/kelektiv/node-cron/commit/c906a92c1f6e315d5c58cdf31ad8d4382b33ba63))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.13.3 ([461e602](https://github.com/kelektiv/node-cron/commit/461e602bd7e0233e8ddcbbeab734618a65484b60))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.13.4 ([7665c00](https://github.com/kelektiv/node-cron/commit/7665c000553e94f3690b2ba0f36a5531121fc6c6))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.13.5 ([5b74613](https://github.com/kelektiv/node-cron/commit/5b746130a61533836d0fb67550dd3d84bda321be))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.17.0 ([40603d9](https://github.com/kelektiv/node-cron/commit/40603d91ef0c0ab22122301043ba0c8447d6a5c1))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.17.2 ([cce46b8](https://github.com/kelektiv/node-cron/commit/cce46b8b9622f54134931ecb08f9070ffbc8ed4f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.18.1 ([9e07aab](https://github.com/kelektiv/node-cron/commit/9e07aabf6da956d981b190c9aaf159de7d409540))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.18.12 ([3d8843d](https://github.com/kelektiv/node-cron/commit/3d8843d6588739cc2ef2691b4665227012919a4d))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.18.6 ([438addc](https://github.com/kelektiv/node-cron/commit/438addcdde9dcf3034c3a761e083ad9a6d572062))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.18.8 ([18f6c48](https://github.com/kelektiv/node-cron/commit/18f6c48651c43edbc80ad2217bdbddd668e956fb))\n* **deps:** update dependency chai to v5.3.3 ([#1008](https://github.com/kelektiv/node-cron/issues/1008)) ([a308ecb](https://github.com/kelektiv/node-cron/commit/a308ecb958992f67ab61ec5ecb709c77c9ddea71))\n* **deps:** update dependency jest to v30.2.0 ([bad0c07](https://github.com/kelektiv/node-cron/commit/bad0c07c43c7df971ee0453c8d4608c3cc31c9fd))\n* **deps:** update dependency typescript to v5.9.2 ([411a2da](https://github.com/kelektiv/node-cron/commit/411a2dad1cb287779803f3ed3e6ef13bc425357d))\n* **deps:** update dependency typescript to v5.9.3 ([2251f01](https://github.com/kelektiv/node-cron/commit/2251f010c5e8b41d24a381dcd46f959239606491))\n* **deps:** update linters ([26069e5](https://github.com/kelektiv/node-cron/commit/26069e5c5ffe17959e812cac12b99796d03ae9ea))\n* **deps:** update semantic-release related packages ([4c56e18](https://github.com/kelektiv/node-cron/commit/4c56e181b8035b9a5fe390d66d09c67e00d3ed2c))\n* **deps:** update semantic-release related packages ([bafbf3b](https://github.com/kelektiv/node-cron/commit/bafbf3bbc565d5c02b35644bb9ba6384b9e5f063))\n* **deps:** update semantic-release related packages (major) ([#1015](https://github.com/kelektiv/node-cron/issues/1015)) ([7b06e1d](https://github.com/kelektiv/node-cron/commit/7b06e1da37cbbee922e02e2ce1ef0aa356748388))\n* **deps:** update tests (major) ([#998](https://github.com/kelektiv/node-cron/issues/998)) ([99670af](https://github.com/kelektiv/node-cron/commit/99670afcaace5a62b2b803f87450b3606abc1f38))\n\n## [4.3.3](https://github.com/kelektiv/node-cron/compare/v4.3.2...v4.3.3) (2025-08-01)\n\n### 🛠 Builds\n\n* **deps:** update dependency [@types](https://github.com/types)/luxon to ~3.7.0 ([9bd0c4e](https://github.com/kelektiv/node-cron/commit/9bd0c4e1c079388e1bbd3d88a153a343c61e396d))\n\n### ♻️ Chores\n\n* **action:** update github/codeql-action action to v3.29.4 ([f28ea6a](https://github.com/kelektiv/node-cron/commit/f28ea6a66071155c4dbed2819040bfd7da37b8d3))\n* **action:** update marocchino/sticky-pull-request-comment action to v2.9.4 ([ceb7a0c](https://github.com/kelektiv/node-cron/commit/ceb7a0c1b37caab545aeebb721561f20d4736306))\n* **action:** update step-security/harden-runner action to v2.13.0 ([91e2402](https://github.com/kelektiv/node-cron/commit/91e2402038284730d38109b6ad0bb76eeb3f8aab))\n* **deps:** lock file maintenance ([34130fc](https://github.com/kelektiv/node-cron/commit/34130fc0d74d7765d4c1b2b21010f4009b821567))\n* **deps:** lock file maintenance ([b79e0c2](https://github.com/kelektiv/node-cron/commit/b79e0c27ebe93373fdcca3a84186ee43e2042deb))\n* **deps:** lock file maintenance ([281e1aa](https://github.com/kelektiv/node-cron/commit/281e1aa5875f2a7f871dde127074afb7ccd179aa))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.16.5 ([16cdbab](https://github.com/kelektiv/node-cron/commit/16cdbab1309e040fbcf316d4915456fb549f5ff2))\n* **deps:** update dependency chai to v5.2.1 ([08b58ce](https://github.com/kelektiv/node-cron/commit/08b58ceb389f52a82a4965817312a1b428661dda))\n* **deps:** update dependency semantic-release to v24.2.7 ([bc3fab6](https://github.com/kelektiv/node-cron/commit/bc3fab6bb8b79aa346f0e5b5c312d0334d3a082d))\n* **deps:** update linters ([b692865](https://github.com/kelektiv/node-cron/commit/b6928658787c921650f6f084362b4e3108899ae7))\n* **deps:** update swc monorepo ([4f3d063](https://github.com/kelektiv/node-cron/commit/4f3d063bd3617ea410ff7b965e44e3d18efbb00d))\n\n## [4.3.2](https://github.com/kelektiv/node-cron/compare/v4.3.1...v4.3.2) (2025-07-13)\n\n### 🛠 Builds\n\n* **deps:** update dependency luxon to ~3.7.0 ([db69c74](https://github.com/kelektiv/node-cron/commit/db69c745016fb1b968aa42376c88da41678dc467))\n\n### ♻️ Chores\n\n* **action:** update github/codeql-action action to v3.29.0 ([#990](https://github.com/kelektiv/node-cron/issues/990)) ([a3fbb3c](https://github.com/kelektiv/node-cron/commit/a3fbb3cc4d98d3ddc485691092ea9a4bec208740))\n* **action:** update github/codeql-action action to v3.29.2 ([0403c53](https://github.com/kelektiv/node-cron/commit/0403c53320e1b403b11ae5f8da031e93c52ba766))\n* **action:** update marocchino/sticky-pull-request-comment action to v2.9.3 ([eda0c4d](https://github.com/kelektiv/node-cron/commit/eda0c4df35e15f3e2d2fff111ea4326b64d6e462))\n* **action:** update ossf/scorecard-action action to v2.4.2 ([#991](https://github.com/kelektiv/node-cron/issues/991)) ([29a3a60](https://github.com/kelektiv/node-cron/commit/29a3a604ef78772f08cbf7f04dd7da001a58e8ba))\n* **action:** update step-security/harden-runner action to v2.12.1 ([ba49a56](https://github.com/kelektiv/node-cron/commit/ba49a5656c163bce2ad70b09be8f7247ec3f9414))\n* **action:** update step-security/harden-runner action to v2.12.2 ([845202e](https://github.com/kelektiv/node-cron/commit/845202ee974b43d366ab1183a7993b8ba6ead7fc))\n* **deps:** lock file maintenance ([#989](https://github.com/kelektiv/node-cron/issues/989)) ([bc1bf72](https://github.com/kelektiv/node-cron/commit/bc1bf72ff7a7595504d936f028a3267d9d823384))\n* **deps:** lock file maintenance ([#999](https://github.com/kelektiv/node-cron/issues/999)) ([e78d986](https://github.com/kelektiv/node-cron/commit/e78d9869d6cd59eaa808ecbb9366399b80e3ba99))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.12.1 ([#992](https://github.com/kelektiv/node-cron/issues/992)) ([b5d3bd3](https://github.com/kelektiv/node-cron/commit/b5d3bd332856dc46aa2742d1992b79ba44e3e48f))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.12.5 ([d374494](https://github.com/kelektiv/node-cron/commit/d374494609e698edcd35a4e5ece78c09851eba00))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.12.9 ([8060c41](https://github.com/kelektiv/node-cron/commit/8060c41685446f4b2c4ea1e0355ad388faa04ad2))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.15.32 ([#993](https://github.com/kelektiv/node-cron/issues/993)) ([ce9743b](https://github.com/kelektiv/node-cron/commit/ce9743ba05275982215c6fa8d2ca8d82013e4705))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.16.0 ([7bae5b1](https://github.com/kelektiv/node-cron/commit/7bae5b1ef895a843f0c3df176f15b50d964c895a))\n* **deps:** update linters ([24eb53f](https://github.com/kelektiv/node-cron/commit/24eb53ff67d5d8f4093278d4efa1109c61b9f7f6))\n* **deps:** update linters ([#995](https://github.com/kelektiv/node-cron/issues/995)) ([9395484](https://github.com/kelektiv/node-cron/commit/939548475833953c9d98d68bd9cc1b9ef1a0e738))\n* **deps:** update node.js to v23.11.1 ([#985](https://github.com/kelektiv/node-cron/issues/985)) ([674a344](https://github.com/kelektiv/node-cron/commit/674a3448b5c286120174bd49ad5d1d99a156fc92))\n* **deps:** update semantic-release related packages ([cc2676a](https://github.com/kelektiv/node-cron/commit/cc2676aa88e6d0c68802bd5937e148ac2284f9b2))\n* **deps:** update semantic-release related packages ([#994](https://github.com/kelektiv/node-cron/issues/994)) ([4d738df](https://github.com/kelektiv/node-cron/commit/4d738df05f794f4edb13fbe0cc02ad163b694f85))\n\n## [4.3.1](https://github.com/kelektiv/node-cron/compare/v4.3.0...v4.3.1) (2025-05-29)\n\n### 🐛 Bug Fixes\n\n* prevent sourcemap error in IDEs ([#988](https://github.com/kelektiv/node-cron/issues/988)) ([0db2c2d](https://github.com/kelektiv/node-cron/commit/0db2c2d3ff7d1f74d6d009c70b371d9e1fca7ae9)), closes [#987](https://github.com/kelektiv/node-cron/issues/987)\n\n### ♻️ Chores\n\n* **action:** update actions/setup-node action to v4.4.0 ([86f8cec](https://github.com/kelektiv/node-cron/commit/86f8cec5225aab3d03cad1aeeb3c74d605ac3177))\n* **action:** update github/codeql-action action to v3.28.16 ([33d396f](https://github.com/kelektiv/node-cron/commit/33d396f0b3c9df243faafed09c1fc09ea363fd85))\n* **action:** update github/codeql-action action to v3.28.17 ([97a9185](https://github.com/kelektiv/node-cron/commit/97a91859e5e08639c9eee1158f60d146cfc543f2))\n* **action:** update github/codeql-action action to v3.28.18 ([6a72709](https://github.com/kelektiv/node-cron/commit/6a72709a34ecf63893345008455c0bba865095a6))\n* **action:** update step-security/harden-runner action to v2.12.0 ([c0ad19d](https://github.com/kelektiv/node-cron/commit/c0ad19d8951ad15ee6fb6365a169a9534f9fa7b2))\n* **deps:** lock file maintenance ([784bc9c](https://github.com/kelektiv/node-cron/commit/784bc9c8ea101be57c890d439fc2ffb17cfe539e))\n* **deps:** lock file maintenance ([7a97350](https://github.com/kelektiv/node-cron/commit/7a973506fc88aeea76e01f3c7350f4c830537eb5))\n* **deps:** lock file maintenance ([40163b4](https://github.com/kelektiv/node-cron/commit/40163b4cd8e63dd7188e30ef7fab8e2f25373c4b))\n* **deps:** lock file maintenance ([9bcb7e3](https://github.com/kelektiv/node-cron/commit/9bcb7e3ba8eaa86fb3ab1f0a54dde12c135430fa))\n* **deps:** lock file maintenance ([#983](https://github.com/kelektiv/node-cron/issues/983)) ([3df1cf6](https://github.com/kelektiv/node-cron/commit/3df1cf62d52ccfb091e487dddcc138773fd9626b))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.25.1 ([162008f](https://github.com/kelektiv/node-cron/commit/162008f2e3a3084a44f5f74a0f2d4fa626a2105a))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.27.0 ([5c1161c](https://github.com/kelektiv/node-cron/commit/5c1161cf6e72e3bec9184adab0a05d548dc2ce0c))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v11.0.2 ([d27afcd](https://github.com/kelektiv/node-cron/commit/d27afcd394e60426632355ceb0f3a30ee9c4cea7))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.11.21 ([a195b0f](https://github.com/kelektiv/node-cron/commit/a195b0f5a526652be7ff7f8976a6010475cce4bb))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.11.29 ([edd52d4](https://github.com/kelektiv/node-cron/commit/edd52d463eeedb18f13495feefbbf1b19a3bd1d2))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.14.1 ([f24a7cb](https://github.com/kelektiv/node-cron/commit/f24a7cbb0a3d34906a65543a558588cbc5740b9f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.15.15 ([#984](https://github.com/kelektiv/node-cron/issues/984)) ([bfb12a7](https://github.com/kelektiv/node-cron/commit/bfb12a76268427470bf298594ea141bd91c5a185))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.15.21 ([86b539a](https://github.com/kelektiv/node-cron/commit/86b539a309fc796846030f34aeea3514ffa6b46c))\n* **deps:** update dependency lint-staged to v15.5.1 ([25a3659](https://github.com/kelektiv/node-cron/commit/25a36594fc87091af092f095dc32fec07cd911df))\n* **deps:** update dependency lint-staged to v15.5.2 ([be017c5](https://github.com/kelektiv/node-cron/commit/be017c52cc709bcf69ba169c97d1a0b6f8250752))\n* **deps:** update linters ([6ed6fdb](https://github.com/kelektiv/node-cron/commit/6ed6fdb4daf73088c0d124987dd9d4c8768f2434))\n* **deps:** update semantic-release related packages ([555bba9](https://github.com/kelektiv/node-cron/commit/555bba9ad22f097edfd0e6b5ec5466fa9d58d0a4))\n* **deps:** update swc monorepo ([edd3284](https://github.com/kelektiv/node-cron/commit/edd328409dbc8fd1f93813905b6e4107b0694d94))\n\n## [4.3.0](https://github.com/kelektiv/node-cron/compare/v4.2.0...v4.3.0) (2025-04-15)\n\n### ✨ Features\n\n* add options to handle cases where jobs could stop unexpectedly ([#980](https://github.com/kelektiv/node-cron/issues/980)) ([994b93a](https://github.com/kelektiv/node-cron/commit/994b93ab2f41af729c4928f5999e2487a67f611d)), closes [#963](https://github.com/kelektiv/node-cron/issues/963) [#962](https://github.com/kelektiv/node-cron/issues/962) [#962](https://github.com/kelektiv/node-cron/issues/962) [#963](https://github.com/kelektiv/node-cron/issues/963)\n\n## [4.2.1-beta.1](https://github.com/kelektiv/node-cron/compare/v4.2.0...v4.2.1-beta.1) (2025-04-15)\n\n### 🐛 Bug Fixes\n\n* prevent jobs from stopping unexpectedly ([#963](https://github.com/kelektiv/node-cron/issues/963)) ([69d2ef5](https://github.com/kelektiv/node-cron/commit/69d2ef5ce5235985ceba391e0e04379550572374))\n\n## [4.2.0](https://github.com/kelektiv/node-cron/compare/v4.1.4...v4.2.0) (2025-04-14)\n\n### ✨ Features\n\n* Allow awaiting job.stop() ([#977](https://github.com/kelektiv/node-cron/issues/977)) ([e296b76](https://github.com/kelektiv/node-cron/commit/e296b76f55783925644ea7d9d10fdfce4172209c)), closes [#976](https://github.com/kelektiv/node-cron/issues/976)\n\n### ♻️ Chores\n\n* **action:** update github/codeql-action action to v3.28.15 ([baf9c7e](https://github.com/kelektiv/node-cron/commit/baf9c7ef2d0edcbe7ab68c86c42febfc7c662383))\n* **action:** update marocchino/sticky-pull-request-comment action to v2.9.2 ([#975](https://github.com/kelektiv/node-cron/issues/975)) ([df57bef](https://github.com/kelektiv/node-cron/commit/df57befb81f7ea66045663add5919bb169b00b0b))\n* **action:** update step-security/harden-runner action to v2.11.1 ([da1764d](https://github.com/kelektiv/node-cron/commit/da1764d8ff959058f263446de8cc388fe1ef86f6))\n* **deps:** lock file maintenance ([c37a3ec](https://github.com/kelektiv/node-cron/commit/c37a3ec62c96c108146da8affafa733503c2d77c))\n* **deps:** lock file maintenance ([#979](https://github.com/kelektiv/node-cron/issues/979)) ([6a355a3](https://github.com/kelektiv/node-cron/commit/6a355a348f8659494da563cc01ff17fff0e429f0))\n* **deps:** pin dependencies ([6ddc31e](https://github.com/kelektiv/node-cron/commit/6ddc31e6e366fd79bbe9cc49034112414b2dbae3))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v2.1.1 ([3fa6836](https://github.com/kelektiv/node-cron/commit/3fa68364269855f91d0d5b19c99d03df8615c67c))\n* **deps:** update dependency [@swc](https://github.com/swc)/core to v1.11.18 ([00d0685](https://github.com/kelektiv/node-cron/commit/00d06855966939040c66e696327e62e195db516f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.14.0 ([7dedc90](https://github.com/kelektiv/node-cron/commit/7dedc9044b23715a2d92ac6cac9552bcbf969641))\n* **deps:** update dependency sinon to v19.0.5 ([448934c](https://github.com/kelektiv/node-cron/commit/448934c42d6592ba5620e65395be7aa153fec099))\n* **deps:** update dependency sinon to v20 ([#974](https://github.com/kelektiv/node-cron/issues/974)) ([ac25eff](https://github.com/kelektiv/node-cron/commit/ac25efff3c4cfae8ca4e4c845812359fcac92d8d))\n* **deps:** update dependency typescript to v5.8.3 ([7b583a4](https://github.com/kelektiv/node-cron/commit/7b583a466f3a8cbb01870a0d1bcd40f5de2af01a))\n* **deps:** update linters ([#978](https://github.com/kelektiv/node-cron/issues/978)) ([cdb638a](https://github.com/kelektiv/node-cron/commit/cdb638a342478573d8276f280236eb3f4902f1ce))\n* **deps:** update node.js to v23.11.0 ([#973](https://github.com/kelektiv/node-cron/issues/973)) ([7d457cf](https://github.com/kelektiv/node-cron/commit/7d457cf34cb2f57c9eec28e46ae35cc8a512e94a))\n\n## [4.1.4](https://github.com/kelektiv/node-cron/compare/v4.1.3...v4.1.4) (2025-04-06)\n\n### 🛠 Builds\n\n* **deps:** update dependency [@types](https://github.com/types)/luxon to ~3.6.0 ([dccbfc7](https://github.com/kelektiv/node-cron/commit/dccbfc79911ee73ff7a3f4863229511ed3ff70cd))\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([c3190bf](https://github.com/kelektiv/node-cron/commit/c3190bffd96621d400fb9ca8f0ecff14a08e487c))\n\n## [4.1.3](https://github.com/kelektiv/node-cron/compare/v4.1.2...v4.1.3) (2025-03-28)\n\n### 🛠 Builds\n\n* **deps:** update dependency luxon to ~3.6.0 ([5935617](https://github.com/kelektiv/node-cron/commit/5935617c3b51e2394b5f63a39bb28cad60d97ef1))\n\n## [4.1.2](https://github.com/kelektiv/node-cron/compare/v4.1.1...v4.1.2) (2025-03-28)\n\n### 🐛 Bug Fixes\n\n* timezone should default to local ([#972](https://github.com/kelektiv/node-cron/issues/972)) ([d2b1aac](https://github.com/kelektiv/node-cron/commit/d2b1aac9c705e111a466daa85b95bed9f7725abd)), closes [#971](https://github.com/kelektiv/node-cron/issues/971) [#971](https://github.com/kelektiv/node-cron/issues/971)\n\n## [4.1.1](https://github.com/kelektiv/node-cron/compare/v4.1.0...v4.1.1) (2025-03-26)\n\n### 🐛 Bug Fixes\n\n* cron should still execute after changing the time back during daylight savings ([#966](https://github.com/kelektiv/node-cron/issues/966)) ([8cf0712](https://github.com/kelektiv/node-cron/commit/8cf07121290beb1b9e1a33bd393503fa031c691a)), closes [#881](https://github.com/kelektiv/node-cron/issues/881) [#881](https://github.com/kelektiv/node-cron/issues/881)\n\n### ♻️ Chores\n\n* **action:** update actions/setup-node action to v4.3.0 ([e70709f](https://github.com/kelektiv/node-cron/commit/e70709f3b004f8ccdddbebf308e85aeabf76ffb5))\n* **action:** update actions/upload-artifact action to v4.6.1 ([06ed76c](https://github.com/kelektiv/node-cron/commit/06ed76c0f0947132eb951b13503f7cb0db797880))\n* **action:** update actions/upload-artifact action to v4.6.2 ([69ea222](https://github.com/kelektiv/node-cron/commit/69ea2223febff26b5dd0074bde002f218ba93c4c))\n* **action:** update github/codeql-action action to v3.28.10 ([1d14a08](https://github.com/kelektiv/node-cron/commit/1d14a0896732686dbc1020fb3ba9d0bf890acec4))\n* **action:** update github/codeql-action action to v3.28.11 ([cd28d4f](https://github.com/kelektiv/node-cron/commit/cd28d4ffb7000c35d766b564f33d2988be5b4601))\n* **action:** update github/codeql-action action to v3.28.13 ([154f885](https://github.com/kelektiv/node-cron/commit/154f885d2da0c203395daa746a9fd3c378d35ab8))\n* **action:** update ossf/scorecard-action action to v2.4.1 ([6a4ec39](https://github.com/kelektiv/node-cron/commit/6a4ec391836848fa4fa0949cc03acb0da1d1cf6b))\n* **deps:** lock file maintenance ([6742c01](https://github.com/kelektiv/node-cron/commit/6742c01a10ec1651de29897d826b0fa21e5f44fc))\n* **deps:** lock file maintenance ([a97cdb1](https://github.com/kelektiv/node-cron/commit/a97cdb19b2c179c1e7fcb07c8f5357299e252a13))\n* **deps:** lock file maintenance ([c585973](https://github.com/kelektiv/node-cron/commit/c585973bf822713a8c0c9006074827732130f345))\n* **deps:** lock file maintenance ([e156aa7](https://github.com/kelektiv/node-cron/commit/e156aa768821ca644c61ddd1387764aa817c85ca))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v19.8.0 ([3984884](https://github.com/kelektiv/node-cron/commit/3984884b29315d73c7323f52d238480e31da1ba3))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.22.0 ([7415480](https://github.com/kelektiv/node-cron/commit/7415480ccafddad023f62df6cd9ca975930c8315))\n* **deps:** update dependency [@eslint](https://github.com/eslint)/js to v9.23.0 ([00fc7ed](https://github.com/kelektiv/node-cron/commit/00fc7ed1e679f31eeef1a057cbb791bd415c76e7))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v2.1.0 ([a9a8608](https://github.com/kelektiv/node-cron/commit/a9a860807f01fe8526e0d8983b6371ceb27c68dd))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.13.11 ([38cf6a6](https://github.com/kelektiv/node-cron/commit/38cf6a6ce15d17d888b6103457c4a0c8cb4a1019))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.13.5 ([a746320](https://github.com/kelektiv/node-cron/commit/a746320f3efe92e47423cf949f42ed1eef945a45))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.13.9 ([4ac339f](https://github.com/kelektiv/node-cron/commit/4ac339fe4848e20caad97844a283df5a4344bedd))\n* **deps:** update dependency lint-staged to v15.5.0 ([5efb27f](https://github.com/kelektiv/node-cron/commit/5efb27f1e1657fc439781ba3ea1fdb2e571ea03e))\n* **deps:** update dependency prettier to v3.5.3 ([d8f2456](https://github.com/kelektiv/node-cron/commit/d8f245616d1415fa08569ac571af4db77ed7f4ab))\n* **deps:** update dependency sinon to v19.0.4 ([5144f4d](https://github.com/kelektiv/node-cron/commit/5144f4dfd90d0755591200cf957b3aff76f52afb))\n* **deps:** update dependency ts-jest to v29.2.6 ([3625528](https://github.com/kelektiv/node-cron/commit/3625528fbcd2fd0e3da63a03e80271541fe92086))\n* **deps:** update dependency typescript to v5.8.2 ([4ef66e8](https://github.com/kelektiv/node-cron/commit/4ef66e84985b5b2259ae67aded6391f5cbd3a5ec))\n* **deps:** update linters ([ecbe916](https://github.com/kelektiv/node-cron/commit/ecbe916c825663839038e94a23aefe5c91744fe5))\n* **deps:** update node.js to v23.10.0 ([#970](https://github.com/kelektiv/node-cron/issues/970)) ([6775fff](https://github.com/kelektiv/node-cron/commit/6775fff2d06798954d60cc66964e2b671a8be1e7))\n* **deps:** update tests ([5d5e555](https://github.com/kelektiv/node-cron/commit/5d5e55527a5dc108f814f5ab8825e405b91a467d))\n\n## [4.1.0](https://github.com/kelektiv/node-cron/compare/v4.0.0...v4.1.0) (2025-02-24)\n\n### ✨ Features\n\n* add isCronTimeValid function to validate cron expressions ([#959](https://github.com/kelektiv/node-cron/issues/959)) ([cbd8106](https://github.com/kelektiv/node-cron/commit/cbd81063229036859fbb6969d4f628a0e7945e57))\n\n### ♻️ Chores\n\n* **action:** update actions/setup-node action to v4.2.0 ([#950](https://github.com/kelektiv/node-cron/issues/950)) ([3a4a701](https://github.com/kelektiv/node-cron/commit/3a4a7015b59c56434b462754728e4ed6483202ef))\n* **action:** update github/codeql-action action to v3.28.9 ([#946](https://github.com/kelektiv/node-cron/issues/946)) ([84ebb32](https://github.com/kelektiv/node-cron/commit/84ebb32e0db478e48e82203099ec0e301021cfc3))\n* **action:** update marocchino/sticky-pull-request-comment action to v2.9.1 ([#947](https://github.com/kelektiv/node-cron/issues/947)) ([7cdcbc2](https://github.com/kelektiv/node-cron/commit/7cdcbc21676fa48f78d58146590e9f59796b56b9))\n* **action:** update step-security/harden-runner action to v2.11.0 ([#948](https://github.com/kelektiv/node-cron/issues/948)) ([b7f9c79](https://github.com/kelektiv/node-cron/commit/b7f9c794f95a95d19a4bfa11d2778dc6a0a21666))\n* **deps:** lock file maintenance ([fa08aa3](https://github.com/kelektiv/node-cron/commit/fa08aa3e0edacdc324381cff33160d49b6db6af0))\n* **deps:** lock file maintenance ([#944](https://github.com/kelektiv/node-cron/issues/944)) ([374ac42](https://github.com/kelektiv/node-cron/commit/374ac429d4ff6f19440d4c18d6e366a69cfea233))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22.13.4 ([#952](https://github.com/kelektiv/node-cron/issues/952)) ([05f1702](https://github.com/kelektiv/node-cron/commit/05f17020fe33ded1839c43132caa0b3dc2770f6e))\n* **deps:** update dependency lint-staged to v15.4.3 ([#953](https://github.com/kelektiv/node-cron/issues/953)) ([b99fc3b](https://github.com/kelektiv/node-cron/commit/b99fc3b82c613b3686930f2055a2b8e10134238b))\n* **deps:** update dependency typescript to v5.7.3 ([#949](https://github.com/kelektiv/node-cron/issues/949)) ([5313b71](https://github.com/kelektiv/node-cron/commit/5313b71e20bcd7a417fce03966cfa5c78062b05a))\n* **deps:** update linters ([#954](https://github.com/kelektiv/node-cron/issues/954)) ([9159759](https://github.com/kelektiv/node-cron/commit/91597592d05770b7e31bbd93eb0b4160da175154))\n* **deps:** update semantic-release related packages ([#951](https://github.com/kelektiv/node-cron/issues/951)) ([92d7ac3](https://github.com/kelektiv/node-cron/commit/92d7ac3f8fa6fcbf37ead72abc3f798fdc05370a))\n* remove bower.json, which is unused ([#955](https://github.com/kelektiv/node-cron/issues/955)) ([8e509f3](https://github.com/kelektiv/node-cron/commit/8e509f3ad7dcd5d250e75a41417830b9b01c2bdb))\n\n## [4.0.0](https://github.com/kelektiv/node-cron/compare/v3.5.0...v4.0.0) (2025-02-19)\n\n### ⚠ Breaking changes\n\n* drop support for Node v16 and rename job.running to job.isActive (#957)\n\n### 📦 Code Refactoring\n\n* drop support for Node v16 and rename job.running to job.isActive ([#957](https://github.com/kelektiv/node-cron/issues/957)) ([605e94e](https://github.com/kelektiv/node-cron/commit/605e94ef3b6469caf2e8526d0935014eea8804c8)), closes [#902](https://github.com/kelektiv/node-cron/issues/902) [#905](https://github.com/kelektiv/node-cron/issues/905)\n\n### ♻️ Chores\n\n* **action:** update actions/checkout action to v4.2.2 ([#927](https://github.com/kelektiv/node-cron/issues/927)) ([ff1721e](https://github.com/kelektiv/node-cron/commit/ff1721e95f1a0d7291f3809dd89af8a0956b8f7f))\n* **action:** update actions/setup-node action to v4.1.0 ([#928](https://github.com/kelektiv/node-cron/issues/928)) ([3e27773](https://github.com/kelektiv/node-cron/commit/3e277738b4a5d096f5990602c3d7aaff02f5961c))\n* **action:** update actions/upload-artifact action to v4.6.0 ([#931](https://github.com/kelektiv/node-cron/issues/931)) ([8283000](https://github.com/kelektiv/node-cron/commit/82830003bcb375b55ae86b2038305af69f587d33))\n* **action:** update amannn/action-semantic-pull-request action to v5.5.3 ([#929](https://github.com/kelektiv/node-cron/issues/929)) ([f1851d7](https://github.com/kelektiv/node-cron/commit/f1851d7d3f4780ef7f6834f35999f93cae5961cf))\n* **action:** update github/codeql-action action to v3.28.1 ([#922](https://github.com/kelektiv/node-cron/issues/922)) ([eefd476](https://github.com/kelektiv/node-cron/commit/eefd47698bad8da9c9cad15fba9ecc0925b95f49))\n* **deps:** lock file maintenance ([c3af5fc](https://github.com/kelektiv/node-cron/commit/c3af5fc439b822683377abea4a7e957ee7743c5c))\n* **deps:** lock file maintenance ([d689a1c](https://github.com/kelektiv/node-cron/commit/d689a1c489f9576de49da8ee8489cbaf700be3c3))\n* **renovate:** improve schedules & automerging to reduce noise ([#942](https://github.com/kelektiv/node-cron/issues/942)) ([c253032](https://github.com/kelektiv/node-cron/commit/c253032d520edd09f29e111fa181f4977a7f9d53))\n\n## [3.5.0](https://github.com/kelektiv/node-cron/compare/v3.4.0...v3.5.0) (2025-01-10)\n\n### ✨ Features\n\n* throw instead of silently rewriting invalid cron expressions ([#937](https://github.com/kelektiv/node-cron/issues/937)) ([dcc5b93](https://github.com/kelektiv/node-cron/commit/dcc5b939fb08a806793799019c9f256bd137c33d))\n\n### ⚙️ Continuous Integrations\n\n* **action:** update step-security/harden-runner action to v2.10.3 ([#943](https://github.com/kelektiv/node-cron/issues/943)) ([cd7ee9f](https://github.com/kelektiv/node-cron/commit/cd7ee9f62fbdd16ec150e9e5aa69d8852f5b3dcb))\n\n### ♻️ Chores\n\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.12 ([2a867f9](https://github.com/kelektiv/node-cron/commit/2a867f9c34c216ac04ce8bce34e0e16578f7dd0c))\n* **deps:** update dependency [@types](https://github.com/types)/node to v22 ([#900](https://github.com/kelektiv/node-cron/issues/900)) ([f7548bd](https://github.com/kelektiv/node-cron/commit/f7548bd3b6981514abd174341b39813d0d6f239a))\n\n## [3.4.0](https://github.com/kelektiv/node-cron/compare/v3.3.2...v3.4.0) (2025-01-09)\n\n### ✨ Features\n\n* error handling on ticks ([#861](https://github.com/kelektiv/node-cron/issues/861)) ([0d3161f](https://github.com/kelektiv/node-cron/commit/0d3161ff7831752edade0333e4ae9ce70e50ac0b)), closes [#426](https://github.com/kelektiv/node-cron/issues/426)\n\n### 📚 Documentation\n\n* **contributing:** add \"Submitting a Pull Request\" & \"Coding Rules\" sections ([#936](https://github.com/kelektiv/node-cron/issues/936)) ([ddd8988](https://github.com/kelektiv/node-cron/commit/ddd89881bcb2f3737e1ea50296fe25294d112bdd))\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([494b4bf](https://github.com/kelektiv/node-cron/commit/494b4bf5d61ba2b4d57e7b46cd433bcb4577d525))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.11 ([2978e92](https://github.com/kelektiv/node-cron/commit/2978e92ba1a26dfe8de7eb6c1ee4b3847cf18279))\n* **deps:** update dependency lint-staged to v15.3.0 ([11f9bad](https://github.com/kelektiv/node-cron/commit/11f9badb761d06cd74a543116854094559acb813))\n* **deps:** update semantic-release related packages ([b830bdb](https://github.com/kelektiv/node-cron/commit/b830bdb41df2ebc48d3510ffd71f3d59255eaf11))\n* **deps:** update tests (major) ([#826](https://github.com/kelektiv/node-cron/issues/826)) ([e47fd5a](https://github.com/kelektiv/node-cron/commit/e47fd5aaefb5452fd06e4a76b847110a13832456))\n\n## [3.3.2](https://github.com/kelektiv/node-cron/compare/v3.3.1...v3.3.2) (2024-12-30)\n\n### 🐛 Bug Fixes\n\n* fix infinite loop on expressions resolving only inside a DST forward jump ([#938](https://github.com/kelektiv/node-cron/issues/938)) ([efb8df5](https://github.com/kelektiv/node-cron/commit/efb8df53405b4ce2ea2e70be9e4d90c124616a51)), closes [/github.com/kelektiv/node-cron/pull/667/files#diff-c14c2dca8456f15417b39cfbd9758009f8eb4f3a190a415768d6e4ae6ae9dceeL473-L477](https://github.com/kelektiv//github.com/kelektiv/node-cron/pull/667/files/issues/diff-c14c2dca8456f15417b39cfbd9758009f8eb4f3a190a415768d6e4ae6ae9dceeL473-L477) [#919](https://github.com/kelektiv/node-cron/issues/919) [#919](https://github.com/kelektiv/node-cron/issues/919)\n\n### ⚙️ Continuous Integrations\n\n* **action:** update marocchino/sticky-pull-request-comment action to v2.9.0 ([#930](https://github.com/kelektiv/node-cron/issues/930)) ([1e7bce9](https://github.com/kelektiv/node-cron/commit/1e7bce9d12a774104f39c1d75b37bdb134e4b270))\n* **renovate:** pin GitHub action digests to semver ([#926](https://github.com/kelektiv/node-cron/issues/926)) ([6541167](https://github.com/kelektiv/node-cron/commit/654116766a299bc5ac5d21a99e2abd7ccc4f43fe))\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([70c3339](https://github.com/kelektiv/node-cron/commit/70c333955612d39b692ab9535b36fe33423eb593))\n* **deps:** lock file maintenance ([afad454](https://github.com/kelektiv/node-cron/commit/afad454e5e4f52e3da54965a0e10540e035c4f58))\n* **deps:** lock file maintenance ([b1dbf69](https://github.com/kelektiv/node-cron/commit/b1dbf69104a58022a638d5b68b59f85089fae7c6))\n* **deps:** pin dependencies ([#915](https://github.com/kelektiv/node-cron/issues/915)) ([dfcbd3c](https://github.com/kelektiv/node-cron/commit/dfcbd3cf7d901415bda5b4929566bd4ad527af62))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v19.6.1 ([7999427](https://github.com/kelektiv/node-cron/commit/799942794b6cff0966fe4977260728c4f9721385))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/release-notes-generator to v14.0.2 ([93c9373](https://github.com/kelektiv/node-cron/commit/93c9373ae020e535683d6c65bff9e1eeabe20d4a))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.10 ([9313ffd](https://github.com/kelektiv/node-cron/commit/9313ffd148e88d68251b13e3b7ec5028d372a9d2))\n* **deps:** update dependency lint-staged to v15.2.11 ([100c9ff](https://github.com/kelektiv/node-cron/commit/100c9ff2f67246b73e6cf053aa581e87a31aed0a))\n\n## [3.3.1](https://github.com/kelektiv/node-cron/compare/v3.3.0...v3.3.1) (2024-12-12)\n\n### 🐛 Bug Fixes\n\n* correct waitForCompletion behavior ([#924](https://github.com/kelektiv/node-cron/issues/924)) ([f6270f8](https://github.com/kelektiv/node-cron/commit/f6270f869d1d472c276f3e153d491f964ba6a4ec)), closes [#923](https://github.com/kelektiv/node-cron/issues/923) [#923](https://github.com/kelektiv/node-cron/issues/923) [#894](https://github.com/kelektiv/node-cron/issues/894)\n\n## [3.3.0](https://github.com/kelektiv/node-cron/compare/v3.2.1...v3.3.0) (2024-12-10)\n\n### ✨ Features\n\n* support async handling and add CronJob status tracking ([#894](https://github.com/kelektiv/node-cron/issues/894)) ([b58fb6b](https://github.com/kelektiv/node-cron/commit/b58fb6b1dc122a6d55bd13134aab1a038e9a531d)), closes [#713](https://github.com/kelektiv/node-cron/issues/713) [#556](https://github.com/kelektiv/node-cron/issues/556)\n\n### ⚙️ Continuous Integrations\n\n* **action:** update github/codeql-action action to v3.27.2 ([#912](https://github.com/kelektiv/node-cron/issues/912)) ([d11ba30](https://github.com/kelektiv/node-cron/commit/d11ba304b380e03e3fa3f7f1185b3eb6cb259405))\n* **action:** update github/codeql-action action to v3.27.5 ([#917](https://github.com/kelektiv/node-cron/issues/917)) ([2a4035e](https://github.com/kelektiv/node-cron/commit/2a4035e4310495847a3cfa54a893e2c216d54c09))\n* **action:** update step-security/harden-runner action to v2.10.2 ([#920](https://github.com/kelektiv/node-cron/issues/920)) ([26a8f9f](https://github.com/kelektiv/node-cron/commit/26a8f9f714c04077f77d24214676feeb1ccf1837))\n* add pre-commit hook to lint and prettify ([#911](https://github.com/kelektiv/node-cron/issues/911)) ([e1140d1](https://github.com/kelektiv/node-cron/commit/e1140d1f6d4fa79d7a2abb876a4aad9c111fec2f)), closes [#907](https://github.com/kelektiv/node-cron/issues/907)\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([94465ae](https://github.com/kelektiv/node-cron/commit/94465aed29609c20fc1f24b52547fb022782a164))\n* **deps:** lock file maintenance ([23d67a4](https://github.com/kelektiv/node-cron/commit/23d67a4c5095ac96bb37ae2dae9b5a72b580aca4))\n* **deps:** lock file maintenance ([135fdf7](https://github.com/kelektiv/node-cron/commit/135fdf7667ce5a4516dab975b1592fe43a7d2882))\n* **deps:** lock file maintenance ([edcff3b](https://github.com/kelektiv/node-cron/commit/edcff3b87750057d82ec8df62770dad63af00d59))\n* **deps:** pin dependency lint-staged to 15.2.10 ([#916](https://github.com/kelektiv/node-cron/issues/916)) ([5cf24da](https://github.com/kelektiv/node-cron/commit/5cf24da52ea060622e21521212824f33020908d2))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v19.6.0 ([9d9ab94](https://github.com/kelektiv/node-cron/commit/9d9ab94196e590b814c2693ff3fcbc7074eca4b4))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.7 ([9181b6a](https://github.com/kelektiv/node-cron/commit/9181b6ac234bee70f3c426059645336610affa8b))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.8 ([5899fc2](https://github.com/kelektiv/node-cron/commit/5899fc22c19fb7b95c0f3e812e2330db3e272e3c))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.9 ([ca5065a](https://github.com/kelektiv/node-cron/commit/ca5065a4d784922feec0257e8ac999f5aa3a9667))\n* **deps:** update dependency husky to v9.1.7 ([a960a29](https://github.com/kelektiv/node-cron/commit/a960a2927cf43f7b212b38aec290e0ad266b33c7))\n* **deps:** update dependency typescript to v5.7.2 ([3447ff5](https://github.com/kelektiv/node-cron/commit/3447ff5f868981a70beeef804bff9139484f6d12))\n\n## [3.2.1](https://github.com/kelektiv/node-cron/compare/v3.2.0...v3.2.1) (2024-11-12)\n\n### 🛠 Builds\n\n* migrate eslint config to flat style ([#913](https://github.com/kelektiv/node-cron/issues/913)) ([38c1044](https://github.com/kelektiv/node-cron/commit/38c104492a229123bbbaf0dad943fee2122ece72)), closes [#899](https://github.com/kelektiv/node-cron/issues/899)\n\n## [3.2.0](https://github.com/kelektiv/node-cron/compare/v3.1.9...v3.2.0) (2024-11-12)\n\n### ✨ Features\n\n* add support for Node v22 ([#914](https://github.com/kelektiv/node-cron/issues/914)) ([9147b20](https://github.com/kelektiv/node-cron/commit/9147b20de6f243a1ab82c86ac836221462ff7695))\n\n### ⚙️ Continuous Integrations\n\n* **action:** update actions/checkout action to v4.2.2 ([#880](https://github.com/kelektiv/node-cron/issues/880)) ([293f54a](https://github.com/kelektiv/node-cron/commit/293f54a3dbd832153ecd995bb77f754d56f03156))\n* **action:** update actions/checkout digest to 11bd719 ([#879](https://github.com/kelektiv/node-cron/issues/879)) ([0287c69](https://github.com/kelektiv/node-cron/commit/0287c69400122a98689c86785bade332875ddd35))\n* **action:** update actions/setup-node digest to 39370e3 ([#889](https://github.com/kelektiv/node-cron/issues/889)) ([0f7a3aa](https://github.com/kelektiv/node-cron/commit/0f7a3aab6825c491ca3475d95fb3c381f96391f6))\n* **action:** update actions/upload-artifact action to v4.4.3 ([#878](https://github.com/kelektiv/node-cron/issues/878)) ([226ad5b](https://github.com/kelektiv/node-cron/commit/226ad5bd3764b19b651f1fa46fbcf67a0e867576))\n* **action:** update step-security/harden-runner action to v2.10.1 ([#882](https://github.com/kelektiv/node-cron/issues/882)) ([b09438e](https://github.com/kelektiv/node-cron/commit/b09438ea8f7121d197685311edc16e6b665f4183))\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([ad613cb](https://github.com/kelektiv/node-cron/commit/ad613cbf4dc6160c63107bae192f58b89b3252d2))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v2.0.3 ([2d00739](https://github.com/kelektiv/node-cron/commit/2d00739e2cdc040f89f0c37e39a6d6f821130f79))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v11.0.1 ([a17bbdd](https://github.com/kelektiv/node-cron/commit/a17bbdd964d019c341480368542a143dbc8cb20a))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.6 ([4509c4d](https://github.com/kelektiv/node-cron/commit/4509c4d7e3dfaef03b1258a00a91cb7783ec3604))\n* **deps:** update dependency husky to v9 ([#844](https://github.com/kelektiv/node-cron/issues/844)) ([9ea2216](https://github.com/kelektiv/node-cron/commit/9ea22166bccce4967be732e3e56205059acafc90))\n\n## [3.1.9](https://github.com/kelektiv/node-cron/compare/v3.1.8...v3.1.9) (2024-11-04)\n\n### 🛠 Builds\n\n* **typescript:** add missing \"types\" property to package.json ([#908](https://github.com/kelektiv/node-cron/issues/908)) ([1953c97](https://github.com/kelektiv/node-cron/commit/1953c973652175cd751beeb12a57e640f1abb958))\n\n### 📚 Documentation\n\n* Add .nvmrc with recommended Node version for development ([#904](https://github.com/kelektiv/node-cron/issues/904)) ([91848e9](https://github.com/kelektiv/node-cron/commit/91848e9267dd8a05acba6b8d705930ddd5c5196e))\n\n### ⚙️ Continuous Integrations\n\n* **action:** update github/codeql-action action to v3.27.0 ([#866](https://github.com/kelektiv/node-cron/issues/866)) ([a6dd871](https://github.com/kelektiv/node-cron/commit/a6dd8710279662a67f7c3ec0cef2f61553bb605b))\n* **action:** update ossf/scorecard-action action to v2.4.0 ([#883](https://github.com/kelektiv/node-cron/issues/883)) ([e0880a1](https://github.com/kelektiv/node-cron/commit/e0880a1fd6722e87d134035ecd4f4232a8e303dd))\n\n### ♻️ Chores\n\n* **config:** migrate renovate config ([#903](https://github.com/kelektiv/node-cron/issues/903)) ([5ce34f4](https://github.com/kelektiv/node-cron/commit/5ce34f4cfa01341a453283b7d8599d1b6380f2aa))\n* **deps:** lock file maintenance ([be77f4a](https://github.com/kelektiv/node-cron/commit/be77f4ad329cd2988be470db51e9750ee34a3fe6))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v2 ([#891](https://github.com/kelektiv/node-cron/issues/891)) ([20c448b](https://github.com/kelektiv/node-cron/commit/20c448bd569efb954a03591ac479d927f9e6bc5f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.2 ([24fa266](https://github.com/kelektiv/node-cron/commit/24fa266d30e4e377d549cf3c230257a3fb0d0621))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.3 ([5bd340e](https://github.com/kelektiv/node-cron/commit/5bd340e3bab6dd340153804667a58ea829948e17))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.4 ([1ce42a4](https://github.com/kelektiv/node-cron/commit/1ce42a483b78506cfdb542bf0a3e6f9c71d6efe3))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.5 ([767c430](https://github.com/kelektiv/node-cron/commit/767c43072a669a6c3ea808185cc8a424293bd7bc))\n* **deps:** update semantic-release related packages (major) ([#835](https://github.com/kelektiv/node-cron/issues/835)) ([73a8d37](https://github.com/kelektiv/node-cron/commit/73a8d3714e0b8e817f882f5ae090320d313c914d))\n\n## [3.1.8](https://github.com/kelektiv/node-cron/compare/v3.1.7...v3.1.8) (2024-10-29)\n\n\n### 🛠 Builds\n\n* **deps:** update dependency luxon to ~3.5.0 ([676045b](https://github.com/kelektiv/node-cron/commit/676045b45506146a4906661efe130be508f7e4fe))\n\n\n### ⚙️ Continuous Integrations\n\n* allow Renovate to auto-merge minor and patch dependency version updates ([#901](https://github.com/kelektiv/node-cron/issues/901)) ([3899b5d](https://github.com/kelektiv/node-cron/commit/3899b5d926cc531850d7551552c178a9b45e2a1d))\n\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([f28aed4](https://github.com/kelektiv/node-cron/commit/f28aed49ad5dfdd3bca38a5331006f876d5b223f))\n* **deps:** lock file maintenance ([a420629](https://github.com/kelektiv/node-cron/commit/a4206292daae053af00705d06af855306de533a9))\n* **deps:** lock file maintenance ([63a1cf1](https://github.com/kelektiv/node-cron/commit/63a1cf1b46d1b9e4f0efe54db6dc6806640e1f15))\n* **deps:** lock file maintenance ([7366c8f](https://github.com/kelektiv/node-cron/commit/7366c8f5dc13dc31d8768a3f5151d4faa5e0478d))\n* **deps:** lock file maintenance ([c44d785](https://github.com/kelektiv/node-cron/commit/c44d78557c65821bd28cd66f10e4a5e67f50835b))\n* **deps:** lock file maintenance ([cf74b29](https://github.com/kelektiv/node-cron/commit/cf74b29a1f6d2efba9de31f4c891571feb1af9e9))\n* **deps:** lock file maintenance ([67b2327](https://github.com/kelektiv/node-cron/commit/67b2327ac88ba9d133f9e4d793c55c1664225966))\n* **deps:** lock file maintenance ([190d845](https://github.com/kelektiv/node-cron/commit/190d8452b024f76615966d1187993850ee9d7850))\n* **deps:** lock file maintenance ([166c4a2](https://github.com/kelektiv/node-cron/commit/166c4a2798c55960ed1a326074dfc20d46112b14))\n* **deps:** lock file maintenance ([b6680c7](https://github.com/kelektiv/node-cron/commit/b6680c7d8f01c6c604810d3bf993ff05dd6c47c1))\n* **deps:** lock file maintenance ([18679e9](https://github.com/kelektiv/node-cron/commit/18679e97d4f29d69ab617ded35fffcf0e58dbd9f))\n* **deps:** lock file maintenance ([d99fc57](https://github.com/kelektiv/node-cron/commit/d99fc5758e38979b92d54a8b8bf50c46359ad9a7))\n* **deps:** lock file maintenance ([8c63a93](https://github.com/kelektiv/node-cron/commit/8c63a93b376e487318f84dd145c559bec73f3633))\n* **deps:** lock file maintenance ([91a5d20](https://github.com/kelektiv/node-cron/commit/91a5d20bc635d11734dba05ab67493c3fa43a6da))\n* **deps:** lock file maintenance ([738f2ac](https://github.com/kelektiv/node-cron/commit/738f2ac6ff2ccb9cdd21b959bca7dc901fc01ea4))\n* **deps:** lock file maintenance ([59df061](https://github.com/kelektiv/node-cron/commit/59df061e0d7e2c581c43ef2815d00dd2d7b0fa7a))\n* **deps:** lock file maintenance ([ad3aac7](https://github.com/kelektiv/node-cron/commit/ad3aac73ac4bd492c516f2864b4041394c9d6299))\n* **deps:** lock file maintenance ([abda61e](https://github.com/kelektiv/node-cron/commit/abda61e936ba909bcfa6858dceced300175c5b71))\n* **deps:** lock file maintenance ([b6954f8](https://github.com/kelektiv/node-cron/commit/b6954f8214ae13ea3ce3e4a5944da4ec497dffad))\n* **deps:** lock file maintenance ([650401f](https://github.com/kelektiv/node-cron/commit/650401f401c0063079421d5d5481ae36d83e5cb1))\n* **deps:** lock file maintenance ([a9cd1a6](https://github.com/kelektiv/node-cron/commit/a9cd1a699c943b105537a933f34cf3f4e4a1fc69))\n* **deps:** lock file maintenance ([652b595](https://github.com/kelektiv/node-cron/commit/652b5958ec567f46302350ab9ba78290a0571a8f))\n* **deps:** lock file maintenance ([e52f3e7](https://github.com/kelektiv/node-cron/commit/e52f3e7d2bfba0048959683a1214f43c9c9a8a82))\n* **deps:** lock file maintenance ([a149323](https://github.com/kelektiv/node-cron/commit/a1493231499a25760066e176b9a29a04f2c99a82))\n* **deps:** lock file maintenance ([dc19fcd](https://github.com/kelektiv/node-cron/commit/dc19fcd35824706fa56f237081a0829e1c330587))\n* **deps:** lock file maintenance ([9aab99b](https://github.com/kelektiv/node-cron/commit/9aab99bb09ae5c2310dd1c9677ec8ff6ae6ad5c9))\n* **deps:** lock file maintenance ([5a8f16d](https://github.com/kelektiv/node-cron/commit/5a8f16d3bacafea8116ff45a7ee1052feceeb1a8))\n* **deps:** lock file maintenance ([e2ab57f](https://github.com/kelektiv/node-cron/commit/e2ab57f5f6e1d870fcb137cc15c0cd15fcf1bc08))\n* **deps:** lock file maintenance ([cdc4477](https://github.com/kelektiv/node-cron/commit/cdc44772b4f212321bda22ac726b3a2f6bb038da))\n* **deps:** lock file maintenance ([83e2a67](https://github.com/kelektiv/node-cron/commit/83e2a679488a1106c17a53aa7e3a9e0c44c7359e))\n* **deps:** lock file maintenance ([4ffc01c](https://github.com/kelektiv/node-cron/commit/4ffc01c1fb37ef42942a44ec1e943c31e2d8b808))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v1.8.2 ([6dfafb6](https://github.com/kelektiv/node-cron/commit/6dfafb6c1428792d75156d7c3d93d95d2a28334d))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.13 ([1de1b8a](https://github.com/kelektiv/node-cron/commit/1de1b8ae2bf0451792d47cb7ede515ed78bf1218))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.14 ([30cd519](https://github.com/kelektiv/node-cron/commit/30cd519c3cf69593712da050a9c0cb9261830ded))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.10 ([78f9456](https://github.com/kelektiv/node-cron/commit/78f9456bd3356d640919ee090ade3bc6747d07f3))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.11 ([a7871f4](https://github.com/kelektiv/node-cron/commit/a7871f4da678114301ea3865278f531dc9099003))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.12 ([b2997da](https://github.com/kelektiv/node-cron/commit/b2997da13c637684e7f3d831f25274a6c7271a73))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.13 ([8876227](https://github.com/kelektiv/node-cron/commit/8876227b161e1dfd2be5c3e435a35922a0cefe80))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.5 ([ca49751](https://github.com/kelektiv/node-cron/commit/ca49751f1696dfce9cbac489efa5ea306f46f62a))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.6 ([4347927](https://github.com/kelektiv/node-cron/commit/43479278b0413313c21ad788a21c9029645c0cdf))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.7 ([9c2357b](https://github.com/kelektiv/node-cron/commit/9c2357b077828d8a3a3a937efb36730ba5c22ef6))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.8 ([3e86607](https://github.com/kelektiv/node-cron/commit/3e8660732917b2196fedfaa0a92033700319760f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.13.0 ([8bc9e52](https://github.com/kelektiv/node-cron/commit/8bc9e52b5383e00d0886b38175e2dfaa68c3d0f5))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.0 ([846691e](https://github.com/kelektiv/node-cron/commit/846691e612a8382b7fd37f43adceccda6d34b508))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.1 ([be4afcd](https://github.com/kelektiv/node-cron/commit/be4afcde2efbfcd54ba3db146d55eba95a9c1fac))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.10 ([ef14320](https://github.com/kelektiv/node-cron/commit/ef143202724f3ccd21f1a5579dd891420facc90b))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.11 ([b60d875](https://github.com/kelektiv/node-cron/commit/b60d87565f7109178bbe389c2e7dd9614caa7a27))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.12 ([6eca2e0](https://github.com/kelektiv/node-cron/commit/6eca2e09802dcc7dafed62a22db1c2904df66f94))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.13 ([1eb978b](https://github.com/kelektiv/node-cron/commit/1eb978b8cddf8a2f2bead841adcad408238333c6))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.14 ([f5ba29b](https://github.com/kelektiv/node-cron/commit/f5ba29b86fc4df45b377dfc1b92460fd31e34daa))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.15 ([636d854](https://github.com/kelektiv/node-cron/commit/636d854a7da98fbe278cc6d1b5a7587103d7dabe))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.2 ([d4dddeb](https://github.com/kelektiv/node-cron/commit/d4dddeb2a743dad3024df1ec97edfa49e6e71122))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.4 ([39d6891](https://github.com/kelektiv/node-cron/commit/39d6891240fcb8b13cf636dc86846c9d7332e57d))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.5 ([e337fc5](https://github.com/kelektiv/node-cron/commit/e337fc54c002baa09080c237495e095eea6d4d1b))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.6 ([4d5849a](https://github.com/kelektiv/node-cron/commit/4d5849ae9245edc578253b7b46ae50f5c95f0080))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.7 ([312df9f](https://github.com/kelektiv/node-cron/commit/312df9f4fd6827e297c6643552ebee264abc97be))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.8 ([2c8dd30](https://github.com/kelektiv/node-cron/commit/2c8dd30ac548d3eb488912549a694aaf85756190))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.14.9 ([18af32b](https://github.com/kelektiv/node-cron/commit/18af32b712f97f0a245d12e34617c7b580ff0e73))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.15.0 ([7ddf6d9](https://github.com/kelektiv/node-cron/commit/7ddf6d9e5929712699086bfcb9169351a784fa38))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.1 ([ce067ef](https://github.com/kelektiv/node-cron/commit/ce067efeb24a139b2179af7c94f3cc1900290272))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.10 ([34fe6e2](https://github.com/kelektiv/node-cron/commit/34fe6e28f6e7607c4fdbeb9fb5e738d2dc8ffff2))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.11 ([6c68d92](https://github.com/kelektiv/node-cron/commit/6c68d92a8f49d0a34cd9ab0f9903073d711eec9b))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.12 ([8eae99a](https://github.com/kelektiv/node-cron/commit/8eae99a8940e1d18b7e80369651632aad1c29cb1))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.13 ([716b77b](https://github.com/kelektiv/node-cron/commit/716b77b6520b2c0f3b7a2b263fd02fa0bb9cfc59))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.14 ([49e621c](https://github.com/kelektiv/node-cron/commit/49e621cd79ea651d5f164025c1c013df3c2f611a))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.15 ([518d610](https://github.com/kelektiv/node-cron/commit/518d610bab9d6f161ea02a99fe84795fc4d3f311))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.2 ([35b662e](https://github.com/kelektiv/node-cron/commit/35b662e1731d810079ed6746206a43d2dca67635))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.3 ([71f95ab](https://github.com/kelektiv/node-cron/commit/71f95ab86d5bfddf5b4c248f013eee8aa881c73f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.4 ([6141a63](https://github.com/kelektiv/node-cron/commit/6141a63bdce1f29865e0cac077c4108b18e9551a))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.5 ([1dd5130](https://github.com/kelektiv/node-cron/commit/1dd5130434cda1cbb3f70a221b9f579198885386))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.6 ([0d4e172](https://github.com/kelektiv/node-cron/commit/0d4e172e1ee9ef33db7bde6b8a0c057a561f6ca9))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.7 ([0a3d240](https://github.com/kelektiv/node-cron/commit/0a3d240d8603a2465b072941cacf39e0dd339b7e))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.16.9 ([d7ea710](https://github.com/kelektiv/node-cron/commit/d7ea71036f8ac5450230e1e00a0d007487b272b1))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.0 ([3823c7b](https://github.com/kelektiv/node-cron/commit/3823c7b8b2178569552e7c62572e586f1e91e3bb))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.17.1 ([7b7275d](https://github.com/kelektiv/node-cron/commit/7b7275ddcb0ccad4cf5be00c8800e56c1704a7a5))\n* **deps:** update dependency chai to v4.5.0 ([91967d3](https://github.com/kelektiv/node-cron/commit/91967d3855d29a21b6421962c07e91ece4cae7e2))\n* **deps:** update dependency eslint to v8.57.1 ([082e62e](https://github.com/kelektiv/node-cron/commit/082e62e49c3a6f0609799e55039d98abd83e9e7d))\n* **deps:** update dependency eslint-plugin-prettier to v5.2.1 ([a82c504](https://github.com/kelektiv/node-cron/commit/a82c504d4fbcc8471606ba865c4b79c0bc89a881))\n* **deps:** update dependency prettier to v3.3.0 ([c2f087d](https://github.com/kelektiv/node-cron/commit/c2f087df04da5e3a723554f66a8134c10cc12ff4))\n* **deps:** update dependency prettier to v3.3.1 ([9d705d5](https://github.com/kelektiv/node-cron/commit/9d705d5e6b434f1b095ce7592f1f591408509d0a))\n* **deps:** update dependency prettier to v3.3.2 ([074ceba](https://github.com/kelektiv/node-cron/commit/074ceba66d7a3761e21310ff14024dcbf231c637))\n* **deps:** update dependency prettier to v3.3.3 ([8dc9d1e](https://github.com/kelektiv/node-cron/commit/8dc9d1effadc0e33bcec69201ec376ea684d1586))\n* **deps:** update dependency sinon to v17.0.2 ([fec3b54](https://github.com/kelektiv/node-cron/commit/fec3b541d278e91a3a0c91944c97292fbdbd6e13))\n* **deps:** update dependency ts-jest to v29.1.3 ([958dc3d](https://github.com/kelektiv/node-cron/commit/958dc3d0a303432af52d9017c763a9322041156e))\n* **deps:** update dependency ts-jest to v29.1.4 ([42bc711](https://github.com/kelektiv/node-cron/commit/42bc7114d35ce063bd4c63ee4f183690975c9026))\n* **deps:** update dependency ts-jest to v29.1.5 ([eb5d897](https://github.com/kelektiv/node-cron/commit/eb5d897cdb5409950374a62866579c4cfcb5bf36))\n* **deps:** update dependency ts-jest to v29.2.0 ([a6285d2](https://github.com/kelektiv/node-cron/commit/a6285d2a633df40ba4405899a31febea9c7c39b4))\n* **deps:** update dependency ts-jest to v29.2.1 ([407ac3c](https://github.com/kelektiv/node-cron/commit/407ac3ccd5cae596464d20a432eec9038fb91a33))\n* **deps:** update dependency ts-jest to v29.2.2 ([712f807](https://github.com/kelektiv/node-cron/commit/712f8074dc4dd52687360659847e2a96a3e0ef3f))\n* **deps:** update dependency ts-jest to v29.2.3 ([7d99dc8](https://github.com/kelektiv/node-cron/commit/7d99dc8ce70d6ea0b36f79e3650e5babd79b48bf))\n* **deps:** update dependency ts-jest to v29.2.4 ([89317c8](https://github.com/kelektiv/node-cron/commit/89317c8b5889119b922a13cd47fc63dd428c21e7))\n* **deps:** update dependency ts-jest to v29.2.5 ([c3ab980](https://github.com/kelektiv/node-cron/commit/c3ab980f787a6b5cd20c1e1b1f1455b4b6de693e))\n* **deps:** update dependency typescript to v5.4.5 ([a32d0d5](https://github.com/kelektiv/node-cron/commit/a32d0d557ea79e3f95371ca99cdfade3340bf0ac))\n* **deps:** update dependency typescript to v5.5.2 ([b6001f0](https://github.com/kelektiv/node-cron/commit/b6001f0a00a9c80808d94b7faf2cba572f959f81))\n* **deps:** update dependency typescript to v5.5.3 ([ce63267](https://github.com/kelektiv/node-cron/commit/ce63267438104f34b3663d56dc9afef08d1b4706))\n* **deps:** update dependency typescript to v5.5.4 ([169eed7](https://github.com/kelektiv/node-cron/commit/169eed7883af7e7ab2516082db547f268ab3e473))\n* **deps:** update dependency typescript to v5.6.2 ([a071dac](https://github.com/kelektiv/node-cron/commit/a071dac40dc088542309f4c20e0bbe2846f0dac3))\n* **deps:** update dependency typescript to v5.6.3 ([1f99a83](https://github.com/kelektiv/node-cron/commit/1f99a832ce9cb8027429631b51f4cc258bb11863))\n\n## [3.1.7](https://github.com/kelektiv/node-cron/compare/v3.1.6...v3.1.7) (2024-04-08)\n\n\n### 🛠 Builds\n\n* **deps:** update dependency [@types](https://github.com/types)/luxon to ~3.4.0 ([#831](https://github.com/kelektiv/node-cron/issues/831)) ([7c31bae](https://github.com/kelektiv/node-cron/commit/7c31bae6e3d6bd2a650c54a347f6cba5ad6fb9f5))\n\n\n### ⚙️ Continuous Integrations\n\n* **action:** prevent duplicate checks on Renovate PRs ([#784](https://github.com/kelektiv/node-cron/issues/784)) ([6b56a36](https://github.com/kelektiv/node-cron/commit/6b56a36281737c87b47e42ba1c8c134c394a1314))\n* **action:** update actions/setup-node digest to 60edb5d ([#821](https://github.com/kelektiv/node-cron/issues/821)) ([f05b75e](https://github.com/kelektiv/node-cron/commit/f05b75eba1bd440685128877d946e1d9341b10dd))\n* **action:** update actions/upload-artifact action to v4 ([#856](https://github.com/kelektiv/node-cron/issues/856)) ([46d6660](https://github.com/kelektiv/node-cron/commit/46d6660a87f6c79d79f996b1e24a194615ed0fcf))\n* **action:** update amannn/action-semantic-pull-request digest to e9fabac ([#849](https://github.com/kelektiv/node-cron/issues/849)) ([d96457c](https://github.com/kelektiv/node-cron/commit/d96457c00ab5598ef9a64b955b5abe3f7d64d915))\n* **action:** update github/codeql-action action to v2.22.6 ([#783](https://github.com/kelektiv/node-cron/issues/783)) [skip ci] ([687fd43](https://github.com/kelektiv/node-cron/commit/687fd435306a355d32e2dbad91f6a68020e73f43))\n* **action:** update github/codeql-action action to v2.22.7 ([#787](https://github.com/kelektiv/node-cron/issues/787)) ([a0204d8](https://github.com/kelektiv/node-cron/commit/a0204d8f0a5bca8467289570dafa9643e233e30b))\n* **action:** update github/codeql-action action to v2.22.8 ([#797](https://github.com/kelektiv/node-cron/issues/797)) ([323f48c](https://github.com/kelektiv/node-cron/commit/323f48c115398cd5925df4faffeef27bebc84b08))\n* **action:** update github/codeql-action action to v3 ([#817](https://github.com/kelektiv/node-cron/issues/817)) ([69d2695](https://github.com/kelektiv/node-cron/commit/69d2695b5e32ec4a7f27a7bd4ba9d0dfa62cd120))\n* **action:** update insurgent-lab/is-in-pr-action action to v0.1.4 ([#790](https://github.com/kelektiv/node-cron/issues/790)) ([8e85b13](https://github.com/kelektiv/node-cron/commit/8e85b1307ed452e94ec58bedc0b8c5d9d0ac7fd0))\n* **action:** update insurgent-lab/is-in-pr-action action to v0.1.5 ([#798](https://github.com/kelektiv/node-cron/issues/798)) ([76751d2](https://github.com/kelektiv/node-cron/commit/76751d272e70a344e6ff0ba623936113dba9c256))\n* **action:** update insurgent-lab/is-in-pr-action action to v0.2.0 ([#853](https://github.com/kelektiv/node-cron/issues/853)) ([cc3fcbd](https://github.com/kelektiv/node-cron/commit/cc3fcbd75d315585a05383acd93174802ecac034))\n* **action:** update marocchino/sticky-pull-request-comment digest to 331f8f5 ([#850](https://github.com/kelektiv/node-cron/issues/850)) ([a477f08](https://github.com/kelektiv/node-cron/commit/a477f0838b5c89625793a2af4cd72a49cfacafc0))\n* **action:** update step-security/harden-runner action to v2.6.1 ([#788](https://github.com/kelektiv/node-cron/issues/788)) [skip ci] ([2e5ca52](https://github.com/kelektiv/node-cron/commit/2e5ca52fea814e954da23cb7a6399bbbeed87431))\n* **action:** update step-security/harden-runner action to v2.7.0 ([#846](https://github.com/kelektiv/node-cron/issues/846)) ([f1a8486](https://github.com/kelektiv/node-cron/commit/f1a84868a7746a6a2e49f7a6483f1dfb50ef0281))\n* fix renovate skipping checks ([#796](https://github.com/kelektiv/node-cron/issues/796)) ([3b00555](https://github.com/kelektiv/node-cron/commit/3b005556cfe03d5e17da1af91357d46b8073691a))\n\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([38979af](https://github.com/kelektiv/node-cron/commit/38979afadb9816a9a9b9c1c1f93a8c5801523cb8))\n* **deps:** lock file maintenance ([dc5b205](https://github.com/kelektiv/node-cron/commit/dc5b2058e6d28b2c52c88efcc87489a8e0739cca))\n* **deps:** lock file maintenance ([77ddb73](https://github.com/kelektiv/node-cron/commit/77ddb73e0dfb21449b23ec3fc53114cc1ca0eb9a))\n* **deps:** lock file maintenance ([03eea6c](https://github.com/kelektiv/node-cron/commit/03eea6c16c8649fb960eba2f24651de438152e0d))\n* **deps:** lock file maintenance ([94e8aac](https://github.com/kelektiv/node-cron/commit/94e8aace1a797fde0b1e83b20e4918f14bb36582))\n* **deps:** lock file maintenance ([142c2d1](https://github.com/kelektiv/node-cron/commit/142c2d1ca8f421c867ed5754c3afb20cd56dd305))\n* **deps:** lock file maintenance ([c70bd32](https://github.com/kelektiv/node-cron/commit/c70bd329c65c5a61e0392d591e268b424c4fa7df))\n* **deps:** lock file maintenance ([e0931ca](https://github.com/kelektiv/node-cron/commit/e0931cae339733f22078574915227fe8dc7c9071))\n* **deps:** lock file maintenance ([effe686](https://github.com/kelektiv/node-cron/commit/effe686e6754cbfccda69cca574df10625bf2617))\n* **deps:** lock file maintenance ([#763](https://github.com/kelektiv/node-cron/issues/763)) ([5d17388](https://github.com/kelektiv/node-cron/commit/5d17388b20b2ec7aca780c1c91ae0eafda4594cb))\n* **deps:** lock file maintenance ([#771](https://github.com/kelektiv/node-cron/issues/771)) ([cf3d5e8](https://github.com/kelektiv/node-cron/commit/cf3d5e8b197666a29468c8594b5a05a7442d9003))\n* **deps:** lock file maintenance ([#781](https://github.com/kelektiv/node-cron/issues/781)) ([6a00c1e](https://github.com/kelektiv/node-cron/commit/6a00c1e2172bc9ae0b631b92ade7af041b3f50ef))\n* **deps:** lock file maintenance ([#793](https://github.com/kelektiv/node-cron/issues/793)) ([bcbc778](https://github.com/kelektiv/node-cron/commit/bcbc77874a06742afd406d5e5ebfd9645409c0be))\n* **deps:** lock file maintenance ([#804](https://github.com/kelektiv/node-cron/issues/804)) ([2e72c8f](https://github.com/kelektiv/node-cron/commit/2e72c8fa16c1b9263577b48cafdcb324c246d920))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18.4.3 ([8c8acf7](https://github.com/kelektiv/node-cron/commit/8c8acf749aa573f24965e1d613ea168bcacfe600))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18.4.4 ([2572023](https://github.com/kelektiv/node-cron/commit/25720237ca5105636820460ed357c54649f50f25))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18.6.0 ([22e8a0f](https://github.com/kelektiv/node-cron/commit/22e8a0f15503a15d82eebd179572c64458f3d529))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18.6.1 ([e34c9d1](https://github.com/kelektiv/node-cron/commit/e34c9d14ec8831855c8109f9a3e9eda9c9a0da05))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v1.8.0 ([689eea4](https://github.com/kelektiv/node-cron/commit/689eea47b04dbc9bac2e4699660c79487871b444))\n* **deps:** update dependency [@fast-check](https://github.com/fast-check)/jest to v1.8.1 ([a6a120c](https://github.com/kelektiv/node-cron/commit/a6a120c7f47242d797f19b2e5f1ad251de7d6967))\n* **deps:** update dependency [@insurgent](https://github.com/insurgent)/conventional-changelog-preset to v9.0.1 ([39ae9c1](https://github.com/kelektiv/node-cron/commit/39ae9c1c5bb7bd0cb145cc20839514d61e3c39b7))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v9.2.4 ([be59173](https://github.com/kelektiv/node-cron/commit/be591738bc8a452d3be5d10a3050485518ca6e5e))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/github to v9.2.6 ([4c994df](https://github.com/kelektiv/node-cron/commit/4c994df2ad64c9efb1cd3ccc5f74ae4c5ec13ccc))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/npm to v11.0.1 ([#770](https://github.com/kelektiv/node-cron/issues/770)) ([72f9dea](https://github.com/kelektiv/node-cron/commit/72f9dea4ba8baf4ab01995b034a27eb488014c73))\n* **deps:** update dependency [@semantic-release](https://github.com/semantic-release)/npm to v11.0.3 ([d62bc05](https://github.com/kelektiv/node-cron/commit/d62bc057663716b7e7a2341f866587e45f36e6ab))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.11 ([8fe499f](https://github.com/kelektiv/node-cron/commit/8fe499f8cb084560b279008c255cf722a6e17093))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.12 ([621f556](https://github.com/kelektiv/node-cron/commit/621f5566dc6feaf4be35a80bf4c37960ef892cf8))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.7 ([#765](https://github.com/kelektiv/node-cron/issues/765)) [skip ci] ([3b9b43d](https://github.com/kelektiv/node-cron/commit/3b9b43d4d797ed6c7d90c4f32c205679abcdb075))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.10.0 ([3111ecd](https://github.com/kelektiv/node-cron/commit/3111ecdd00e950c8d9bf292b9e61f4c27c4e7330))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.10.4 ([b0853e7](https://github.com/kelektiv/node-cron/commit/b0853e7a4f28bec7007d70b4ed0df6689d909523))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.10.5 ([35ee733](https://github.com/kelektiv/node-cron/commit/35ee733ca8a0cffcfecb4ce4f2e5aa20cfe09eed))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.10.7 ([603fdfa](https://github.com/kelektiv/node-cron/commit/603fdfa1b4ce38f116409dee572d0e82c61953fe))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.0 ([a1a38fe](https://github.com/kelektiv/node-cron/commit/a1a38fe898939c0cebbc27addf93907fe517a19c))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.13 ([c42b5e9](https://github.com/kelektiv/node-cron/commit/c42b5e933730cb2c0be507dbd374be499bc2e287))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.17 ([f37f31c](https://github.com/kelektiv/node-cron/commit/f37f31cfb407f30ebdeb35df333097dcbabd5bac))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.19 ([c3bdd6d](https://github.com/kelektiv/node-cron/commit/c3bdd6d72d67dc637a09c1e2f4513e4b444aa031))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.24 ([fb4a087](https://github.com/kelektiv/node-cron/commit/fb4a087cbe88c2bb78c36972c5c1f6eae7c93704))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.25 ([4b22794](https://github.com/kelektiv/node-cron/commit/4b22794efef010656334a7e90297d04fb03a4d0f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.26 ([166563d](https://github.com/kelektiv/node-cron/commit/166563d17f67f7191e3df4c99a624e07214a1a77))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.27 ([8c14927](https://github.com/kelektiv/node-cron/commit/8c1492721c99e49f53d5abbd10e6917584edffeb))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.28 ([bf7efeb](https://github.com/kelektiv/node-cron/commit/bf7efeb152ec442c8a0c502c969fedba6b47af72))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.30 ([ed26da9](https://github.com/kelektiv/node-cron/commit/ed26da99a7bb6c60f055535826c03433239aa869))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.5 ([8838ae5](https://github.com/kelektiv/node-cron/commit/8838ae599f650a0098950e314682419405104417))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.11.8 ([006f6fc](https://github.com/kelektiv/node-cron/commit/006f6fc272cb69170a1ae4b2d52c63d849cc6bc2))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.2 ([f6bf108](https://github.com/kelektiv/node-cron/commit/f6bf10845dc76a4ef9768a424281b722752a737e))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.3 ([30648f8](https://github.com/kelektiv/node-cron/commit/30648f83d414c7f97b26913ba642df6182fc1601))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.12.4 ([7d5cf86](https://github.com/kelektiv/node-cron/commit/7d5cf86ffe7a89bdd96fe06049d9273354bdbb67))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.8.10 ([#768](https://github.com/kelektiv/node-cron/issues/768)) [skip ci] ([6a91b78](https://github.com/kelektiv/node-cron/commit/6a91b78445208236dd3c82102e7edec9cd48af4f))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.9.0 ([6d972d5](https://github.com/kelektiv/node-cron/commit/6d972d5d5f9eb6fac930722d781e1e89a9979ba7))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.9.1 ([1817d85](https://github.com/kelektiv/node-cron/commit/1817d85f70fddbd5238bc72032f7e635e1a2e007))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.9.2 ([a9fb08b](https://github.com/kelektiv/node-cron/commit/a9fb08b316f5045daa3737fe6f7cc50e3f9217d0))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.9.4 ([05124bb](https://github.com/kelektiv/node-cron/commit/05124bbd4db2a0ac18c69beaed7f2253d8e4ce31))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.9.5 ([a32ba3d](https://github.com/kelektiv/node-cron/commit/a32ba3d6797816828b5c3c162173af8abea074bc))\n* **deps:** update dependency [@types](https://github.com/types)/sinon to v17 ([#774](https://github.com/kelektiv/node-cron/issues/774)) [skip ci] ([6ab97db](https://github.com/kelektiv/node-cron/commit/6ab97db986e4c09388653f9bc5deffd1855bd1ea))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.11.0 ([d41dfa3](https://github.com/kelektiv/node-cron/commit/d41dfa359cbd9c3128482d9815f5f2704528ec5a))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.12.0 ([bd2e981](https://github.com/kelektiv/node-cron/commit/bd2e981b595c55532d4951140dc08063edd22f5d))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.13.1 ([#806](https://github.com/kelektiv/node-cron/issues/806)) ([d696565](https://github.com/kelektiv/node-cron/commit/d696565fff32edf11f885938f5b9e8d8ea979e22))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.13.2 ([d614393](https://github.com/kelektiv/node-cron/commit/d6143937d8774d8e4f5dcf3fcb86b296e2811c29))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.9.1 ([#766](https://github.com/kelektiv/node-cron/issues/766)) [skip ci] ([4d2bfe6](https://github.com/kelektiv/node-cron/commit/4d2bfe6bf6029b497fee8e306816cd36113b9ee1))\n* **deps:** update dependency chai to v4.4.1 ([cd26e74](https://github.com/kelektiv/node-cron/commit/cd26e749da0e81c6a037d0570e3a68b729540279))\n* **deps:** update dependency eslint to v8.53.0 ([#773](https://github.com/kelektiv/node-cron/issues/773)) [skip ci] ([8dea8d7](https://github.com/kelektiv/node-cron/commit/8dea8d74cd6bb501bec86ba0fd145f73510c8b0d))\n* **deps:** update dependency eslint to v8.54.0 ([ef4a99c](https://github.com/kelektiv/node-cron/commit/ef4a99c48b3538cb6c7dda458a5acd09d07674b7))\n* **deps:** update dependency eslint to v8.55.0 ([f5578ac](https://github.com/kelektiv/node-cron/commit/f5578ac8aa6db18bbcf3cae3ac13b6e60a119e7b))\n* **deps:** update dependency eslint-config-prettier to v9.1.0 ([ba1df8b](https://github.com/kelektiv/node-cron/commit/ba1df8b49858088256436e1a060ecd6bbef8537d))\n* **deps:** update dependency eslint-plugin-jest to v27.6.0 ([#762](https://github.com/kelektiv/node-cron/issues/762)) ([615b06f](https://github.com/kelektiv/node-cron/commit/615b06f37dbf2f6fe513e4670cf718f631ae5fdd))\n* **deps:** update dependency eslint-plugin-jest to v27.8.0 ([ca9c72d](https://github.com/kelektiv/node-cron/commit/ca9c72d0b9ee44cbb09efabebc616a18b7e5183d))\n* **deps:** update dependency prettier to v3.1.1 ([f31bd3b](https://github.com/kelektiv/node-cron/commit/f31bd3be5789c6167e6c4e06c4145f63eb2d98fa))\n* **deps:** update dependency semantic-release to v22.0.6 ([#767](https://github.com/kelektiv/node-cron/issues/767)) [skip ci] ([230291a](https://github.com/kelektiv/node-cron/commit/230291a3a1216b9aca0e50cb1d59cb7793fe1c5d))\n* **deps:** update dependency semantic-release to v22.0.9 ([ac87eba](https://github.com/kelektiv/node-cron/commit/ac87ebaa31412e311de572745262791339f0151c))\n* **deps:** update dependency sinon to v17.0.1 ([#769](https://github.com/kelektiv/node-cron/issues/769)) [skip ci] ([bf2bdfb](https://github.com/kelektiv/node-cron/commit/bf2bdfbab6221130b926c14ae128c0d24281fcda))\n* **deps:** update dependency ts-jest to v29.1.2 ([6f8af23](https://github.com/kelektiv/node-cron/commit/6f8af2332c09edc880625eb8f3d83a41caab6d35))\n* **deps:** update dependency typescript to v5.3.2 ([#802](https://github.com/kelektiv/node-cron/issues/802)) ([0f541a4](https://github.com/kelektiv/node-cron/commit/0f541a47463f17485db6fdbb23ab2b8704de209b))\n* **deps:** update dependency typescript to v5.3.3 ([4f470c6](https://github.com/kelektiv/node-cron/commit/4f470c6d0ce046ade5b71012ce3b551e0561c6cc))\n* **deps:** update dependency typescript to v5.4.2 ([98dfa32](https://github.com/kelektiv/node-cron/commit/98dfa32e15ec4c2591a1b916017fa703e59d3be3))\n* **deps:** update dependency typescript to v5.4.3 ([412c453](https://github.com/kelektiv/node-cron/commit/412c453de53876b760e4ec4f27c339b409d762fd))\n* **deps:** update dependency typescript to v5.4.4 ([6b172b1](https://github.com/kelektiv/node-cron/commit/6b172b13b893e3ca417b93bd6a5c1094526ad049))\n* **deps:** update linters ([2c2fe1b](https://github.com/kelektiv/node-cron/commit/2c2fe1bb5c789a8d538e5103b05ed71f5b7601c0))\n* **deps:** update linters ([66a470a](https://github.com/kelektiv/node-cron/commit/66a470af968eabd4014228f1ccb6c3c9ae006f28))\n* **deps:** update linters ([3010a70](https://github.com/kelektiv/node-cron/commit/3010a70500d017e4ffb1f12d83b2abcb2ccf204f))\n* **deps:** update linters ([f8609df](https://github.com/kelektiv/node-cron/commit/f8609df889d66596fc90ecdc4885c70f3414f4ca))\n* **deps:** update linters ([8937be5](https://github.com/kelektiv/node-cron/commit/8937be5cfe722e1bd95a73c49cf28759b71aaf06))\n* **deps:** update linters ([834e0f4](https://github.com/kelektiv/node-cron/commit/834e0f411cde6841178187c77618eecab583cae2))\n* **deps:** update linters ([fe0d705](https://github.com/kelektiv/node-cron/commit/fe0d70532156249981c0ab47f14822ecfa80cce2))\n* **deps:** update linters ([767ad39](https://github.com/kelektiv/node-cron/commit/767ad39a90ad6b55295cbb973adae508b08772bf))\n* **deps:** update semantic-release related packages ([38096a9](https://github.com/kelektiv/node-cron/commit/38096a940a9ae089b478dc8177635120243b01bd))\n* **deps:** update semantic-release related packages ([a5cd89d](https://github.com/kelektiv/node-cron/commit/a5cd89d450bfd9355e6b6aad5d9596cb2213e800))\n* **deps:** update semantic-release related packages ([#772](https://github.com/kelektiv/node-cron/issues/772)) [skip ci] ([4a654a7](https://github.com/kelektiv/node-cron/commit/4a654a7b474da06f35a058a37e8e4d2f2609d8fd))\n* **deps:** update semantic-release related packages ([#777](https://github.com/kelektiv/node-cron/issues/777)) ([898254c](https://github.com/kelektiv/node-cron/commit/898254c7a358863dd8e0afff4e24cf169552fbe9))\n* **deps:** update tests ([eb417b6](https://github.com/kelektiv/node-cron/commit/eb417b69c578f4b5b15d376c9561ab5ac64647bb))\n* **deps:** update tests ([0cdd4a3](https://github.com/kelektiv/node-cron/commit/0cdd4a330ef3b7aa6b871ef9c23588a94b2bf6a0))\n* **deps:** update tests ([#800](https://github.com/kelektiv/node-cron/issues/800)) ([ea1a22b](https://github.com/kelektiv/node-cron/commit/ea1a22be10aa96ad17689ac2c9e424f4394234dd))\n* reduce renovate updates noise ([#750](https://github.com/kelektiv/node-cron/issues/750)) ([661722f](https://github.com/kelektiv/node-cron/commit/661722f32020f5894bdcc24169116bad5974d1a3))\n* reflect insurgentlab scope update in config files ([#785](https://github.com/kelektiv/node-cron/issues/785)) ([edf67d0](https://github.com/kelektiv/node-cron/commit/edf67d006fafc0e4785bcbd4148feb20cb7ae2f5))\n\n## [3.1.6](https://github.com/kelektiv/node-cron/compare/v3.1.5...v3.1.6) (2023-10-29)\n\n\n### 🐛 Bug Fixes\n\n* revert runOnce breaking changes ([#760](https://github.com/kelektiv/node-cron/issues/760)) ([7cb53ec](https://github.com/kelektiv/node-cron/commit/7cb53ec9944b19ed5ba92b2466e73fc158ef1d11))\n\n\n### ⚙️ Continuous Integrations\n\n* **action:** update actions/checkout action to v4 ([#755](https://github.com/kelektiv/node-cron/issues/755)) ([d0d70c6](https://github.com/kelektiv/node-cron/commit/d0d70c67532b9a2563ed7a4273901f85b314d1b9))\n* **action:** update github/codeql-action action to v2.22.4 ([#752](https://github.com/kelektiv/node-cron/issues/752)) ([04454c3](https://github.com/kelektiv/node-cron/commit/04454c34a5cbfcffb6938173393380840a7c8d75))\n* **action:** update github/codeql-action action to v2.22.5 ([#758](https://github.com/kelektiv/node-cron/issues/758)) ([2dff183](https://github.com/kelektiv/node-cron/commit/2dff1838a6b8ee400ad721c7a41065ee73a42b8a))\n* **action:** update ossf/scorecard-action action to v2.3.1 ([#754](https://github.com/kelektiv/node-cron/issues/754)) ([41d21f1](https://github.com/kelektiv/node-cron/commit/41d21f16b58dc76e48c81751c8782b6c6c1ac7ca))\n\n\n### ♻️ Chores\n\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18.2.0 ([#759](https://github.com/kelektiv/node-cron/issues/759)) ([4cb466e](https://github.com/kelektiv/node-cron/commit/4cb466e183016861098cb4252b48cb54fb0ea96d))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.8.8 ([#756](https://github.com/kelektiv/node-cron/issues/756)) ([361728e](https://github.com/kelektiv/node-cron/commit/361728ebf8bc6a3202001fccaf6661e9417054a1))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.8.9 ([#757](https://github.com/kelektiv/node-cron/issues/757)) ([e2b1bac](https://github.com/kelektiv/node-cron/commit/e2b1bac21b59b9bd02a9000ec1c885961d07e121))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.9.0 ([#753](https://github.com/kelektiv/node-cron/issues/753)) ([9cec04f](https://github.com/kelektiv/node-cron/commit/9cec04f1578f26f50d3e338ad3a9053ef74bbb01))\n\n## [3.1.5](https://github.com/kelektiv/node-cron/compare/v3.1.4...v3.1.5) (2023-10-26)\n\n\n### 🐛 Bug Fixes\n\n* detect multiple zeros as an invalid step ([#743](https://github.com/kelektiv/node-cron/issues/743)) [skip ci] ([b0bf677](https://github.com/kelektiv/node-cron/commit/b0bf677ee7b7c322dbe2c9feb13257787edc4fb8))\n* re-add runOnce property to CronJob ([#751](https://github.com/kelektiv/node-cron/issues/751)) ([a61d8c9](https://github.com/kelektiv/node-cron/commit/a61d8c95057b6055a2fe0f18896a098f5d0266e0))\n\n\n### 📚 Documentation\n\n* **readme:** API documentation overhaul ([#716](https://github.com/kelektiv/node-cron/issues/716)) [skip ci] ([23fb0a3](https://github.com/kelektiv/node-cron/commit/23fb0a383fc5dea2f677d69638a1c34ec49b6425))\n\n\n### ⚙️ Continuous Integrations\n\n* **action:** update actions/setup-node action to v4 ([#749](https://github.com/kelektiv/node-cron/issues/749)) ([ef850f3](https://github.com/kelektiv/node-cron/commit/ef850f32f0b429825e2bea59fedbf53fa0053894))\n\n\n### ♻️ Chores\n\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v18 ([#747](https://github.com/kelektiv/node-cron/issues/747)) ([5ff1cf8](https://github.com/kelektiv/node-cron/commit/5ff1cf826b376b1c3a03003b771ce8ca96edfaf5))\n* **deps:** update dependency sinon to v17 ([#748](https://github.com/kelektiv/node-cron/issues/748)) ([9d61ff9](https://github.com/kelektiv/node-cron/commit/9d61ff976371ebf93bcfedf8386ba4fe426dcac5))\n* **deps:** update linters ([7bdc726](https://github.com/kelektiv/node-cron/commit/7bdc726e8960d89b489a648eb5918090c7ee01c9))\n* improve ossf scorecard's score ([#715](https://github.com/kelektiv/node-cron/issues/715)) [skip ci] ([1284df4](https://github.com/kelektiv/node-cron/commit/1284df476ec7bbcbc6493ab38af4cb4d3542580b))\n\n## [3.1.4](https://github.com/kelektiv/node-cron/compare/v3.1.3...v3.1.4) (2023-10-24)\n\n\n### 🐛 Bug Fixes\n\n* run once when actual date is given to setTime ([#740](https://github.com/kelektiv/node-cron/issues/740)) ([ee54dd5](https://github.com/kelektiv/node-cron/commit/ee54dd52956a9a203a72d4a38673bf0268cd6487))\n\n\n### ⚙️ Continuous Integrations\n\n* **action:** update actions/checkout action to v4 ([#735](https://github.com/kelektiv/node-cron/issues/735)) ([144ba67](https://github.com/kelektiv/node-cron/commit/144ba677cbafc16e0c2e9d5372561589715de536))\n\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([#741](https://github.com/kelektiv/node-cron/issues/741)) ([6d94742](https://github.com/kelektiv/node-cron/commit/6d94742fe1c7959569e7c3b922a0cfee4143ba0f))\n* **deps:** update dependency [@types](https://github.com/types)/jest to v29.5.6 ([#736](https://github.com/kelektiv/node-cron/issues/736)) ([57c0efa](https://github.com/kelektiv/node-cron/commit/57c0efafcd875fed2550d0bb7ee383ccd2fd4790))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.8.7 ([#737](https://github.com/kelektiv/node-cron/issues/737)) ([21c4065](https://github.com/kelektiv/node-cron/commit/21c4065399fb8eb8aa10a4cd1c14d42f175263eb))\n* **deps:** update dependency [@typescript-eslint](https://github.com/typescript-eslint)/eslint-plugin to v6.8.0 ([#734](https://github.com/kelektiv/node-cron/issues/734)) ([12e7487](https://github.com/kelektiv/node-cron/commit/12e7487496f9e76bc11ba48fcb99a40bcdd8674f))\n* **deps:** update tests ([#738](https://github.com/kelektiv/node-cron/issues/738)) ([3815e2a](https://github.com/kelektiv/node-cron/commit/3815e2a56560ec105322c9dee3b45027ea069c1b))\n\n## [3.1.3](https://github.com/kelektiv/node-cron/compare/v3.1.2...v3.1.3) (2023-10-19)\n\n\n### 🐛 Bug Fixes\n\n* allow callbacks to return a promise ([#733](https://github.com/kelektiv/node-cron/issues/733)) ([8dd8b75](https://github.com/kelektiv/node-cron/commit/8dd8b754b69e2cbe503f344f2346a035cf5a3ec6))\n\n\n### ⚙️ Continuous Integrations\n\n* **renovate:** update configuration ([#732](https://github.com/kelektiv/node-cron/issues/732)) [skip ci] ([2ff9c6e](https://github.com/kelektiv/node-cron/commit/2ff9c6eb94fbc833c6a7fff30242e2b4b8367dc6))\n\n## [3.1.2](https://github.com/kelektiv/node-cron/compare/v3.1.1...v3.1.2) (2023-10-19)\n\n\n### 🛠 Builds\n\n* **deps:** update dependency luxon to ~3.4.0 ([#730](https://github.com/kelektiv/node-cron/issues/730)) ([c3806c5](https://github.com/kelektiv/node-cron/commit/c3806c54574d2f1886272f0b6fc0742b84dcba03))\n\n\n### ♻️ Chores\n\n* **deps:** lock file maintenance ([#731](https://github.com/kelektiv/node-cron/issues/731)) ([b6bc715](https://github.com/kelektiv/node-cron/commit/b6bc715e20f48550b4265b90bd5a21b635ece14c))\n* **deps:** pin dependencies ([#719](https://github.com/kelektiv/node-cron/issues/719)) [skip ci] ([5003745](https://github.com/kelektiv/node-cron/commit/5003745208f2312bfc41dd88b68520c00d72cf5c))\n* **deps:** pin dependencies ([#720](https://github.com/kelektiv/node-cron/issues/720)) [skip ci] ([4f977ef](https://github.com/kelektiv/node-cron/commit/4f977efb245d0c8a4113ef979e92c90c4ce6b0ee))\n* **deps:** pin dependencies ([#721](https://github.com/kelektiv/node-cron/issues/721)) [skip ci] ([60fbf7f](https://github.com/kelektiv/node-cron/commit/60fbf7fa9f34cb1a214673eb556aa62eab3b848b))\n* **deps:** update dependency [@commitlint](https://github.com/commitlint)/cli to v17.8.0 ([#723](https://github.com/kelektiv/node-cron/issues/723)) [skip ci] ([a7a18cb](https://github.com/kelektiv/node-cron/commit/a7a18cbd8d550214377ff5c8d77f29952d2b91c7))\n* **deps:** update dependency [@types](https://github.com/types)/node to v20.8.6 ([#724](https://github.com/kelektiv/node-cron/issues/724)) [skip ci] ([b5e4c9f](https://github.com/kelektiv/node-cron/commit/b5e4c9f463543f6d1ba2bc9eebbd5eb6f3d1b5ea))\n* **deps:** update dependency sinon to v16 ([#726](https://github.com/kelektiv/node-cron/issues/726)) [skip ci] ([d114a12](https://github.com/kelektiv/node-cron/commit/d114a1291d4257a899150468e4736c594b542355))\n* **deps:** update dependency typescript to v5.2.2 ([#729](https://github.com/kelektiv/node-cron/issues/729)) [skip ci] ([d1b267e](https://github.com/kelektiv/node-cron/commit/d1b267e6490347786e9aa77be6a1016598b968ea))\n* **deps:** update linters ([#728](https://github.com/kelektiv/node-cron/issues/728)) [skip ci] ([9ab00e8](https://github.com/kelektiv/node-cron/commit/9ab00e8e2adac547017bfbe5b6d97d7bc9c6fb28))\n* **deps:** update linters (major) ([#727](https://github.com/kelektiv/node-cron/issues/727)) [skip ci] ([a75418a](https://github.com/kelektiv/node-cron/commit/a75418aca0f51b98625979087aa86e166ac17c56))\n* **deps:** update tests ([#722](https://github.com/kelektiv/node-cron/issues/722)) [skip ci] ([6b4c6fa](https://github.com/kelektiv/node-cron/commit/6b4c6fae750d005c3e30f12b868e4f01a3b99a68))\n\n## [3.1.1](https://github.com/kelektiv/node-cron/compare/v3.1.0...v3.1.1) (2023-10-12)\n\n\n### 🐛 Bug Fixes\n\n* fix lastDate() value for intervals > 25 days ([#711](https://github.com/kelektiv/node-cron/issues/711)) ([141aa00](https://github.com/kelektiv/node-cron/commit/141aa00f55fa105d89df7e257d82c94ad2bb2b3a))\n* fix object constructor typings & make OC generic type optional ([#712](https://github.com/kelektiv/node-cron/issues/712)) ([6536084](https://github.com/kelektiv/node-cron/commit/653608451caecd51f50884d83563c03c6c27bc54))\n\n\n### 📚 Documentation\n\n* **readme:** update nextDates documentation ([#702](https://github.com/kelektiv/node-cron/issues/702)) ([1ad2e22](https://github.com/kelektiv/node-cron/commit/1ad2e228f75bcaab3eb97a27665c06b7892678c5))\n\n## [3.1.0](https://github.com/kelektiv/node-cron/compare/v3.0.0...v3.1.0) (2023-10-09)\n\n\n### ✨ Features\n\n* improve context, onTick & onComplete typings ([#705](https://github.com/kelektiv/node-cron/issues/705)) ([82c78d7](https://github.com/kelektiv/node-cron/commit/82c78d79594c2d1c1e36baa67ecd76c033a171e4))\n\n\n### 🚨 Tests\n\n* check at runtime that all tests call expect ([#706](https://github.com/kelektiv/node-cron/issues/706)) [skip ci] ([cc4e62f](https://github.com/kelektiv/node-cron/commit/cc4e62fd1fd713d4c8a324ba36c0caa8b0c364e9))\n\n\n### ♻️ Chores\n\n* **deps:** update semantic-release related packages ([#709](https://github.com/kelektiv/node-cron/issues/709)) [skip ci] ([b94a48a](https://github.com/kelektiv/node-cron/commit/b94a48a6b24e20c1f3a9c5109158ff359d048bac))\n* reflect repository label changes ([#708](https://github.com/kelektiv/node-cron/issues/708)) ([85c9e18](https://github.com/kelektiv/node-cron/commit/85c9e18023aea907c10ca77bc92db7a8086f1b6b))\n\n## [3.0.0](https://github.com/kelektiv/node-cron/compare/v2.4.4...v3.0.0) (2023-09-30)\n\n\n### ⚠ Breaking changes\n\n* `utcOffset` parameter no longer accepts a string\n* `utcOffset` values between -60 and 60 are no longer\ntreated as hours\n* providing both `timeZone` and `utcOffset` parameters\nnow throws an error\n* removed `cron.job()` method in favor of `new CronJob(...args)` /\n`CronJob.from(argsObject)`\n* removed `cron.time()` method in favor of `new CronTime()`\n* `CronJob`: constructor no longer accepts an object as its first and\nonly params. Use `CronJob.from(argsObject)` instead.\n* `CronJob`: callbacks are now called in the order they were registered\n* return empty array from nextDates when called without argument (#519)\n* UNIX standard alignments (#667)\n\n### ✨ Features\n\n* expose useful types ([737b344](https://github.com/kelektiv/node-cron/commit/737b34482c47033f9affab4426a3201681f42e97))\n* rework utcOffset parameter ([#699](https://github.com/kelektiv/node-cron/issues/699)) ([671e933](https://github.com/kelektiv/node-cron/commit/671e933d9107b1e4e1166ab681f9e14a8a3a7c16))\n* UNIX standard alignments ([#667](https://github.com/kelektiv/node-cron/issues/667)) ([ff615f1](https://github.com/kelektiv/node-cron/commit/ff615f1592287262b7ebc95312cdac0f9c59d272))\n\n\n### 🐛 Bug Fixes\n\n* return empty array from nextDates when called without argument ([#519](https://github.com/kelektiv/node-cron/issues/519)) ([c2891ba](https://github.com/kelektiv/node-cron/commit/c2891bacbc0d88616b69449fc6237f716dfe4faf))\n\n\n### 📦 Code Refactoring\n\n* migrate to TypeScript ([#694](https://github.com/kelektiv/node-cron/issues/694)) ([2d77894](https://github.com/kelektiv/node-cron/commit/2d778942c523f8480051216b4ced46c1d2651153))\n\n\n### 📚 Documentation\n\n* **readme:** remove outdated informations ([#695](https://github.com/kelektiv/node-cron/issues/695)) ([b5ceaf1](https://github.com/kelektiv/node-cron/commit/b5ceaf16913f78ca8d4037594a86df61f116d08e))\n\n\n### 🚨 Tests\n\n* update new test for cron standard alignments ([4a406c1](https://github.com/kelektiv/node-cron/commit/4a406c1f7e7b77ec9c7433c61a4929a341bfe300))\n\n\n### ♻️ Chores\n\n* improve GitHub community standards ([#698](https://github.com/kelektiv/node-cron/issues/698)) ([6bdef77](https://github.com/kelektiv/node-cron/commit/6bdef779b813ee84c03b7c708176410aa24a8cfe))\n* update contributors list ([dab3d69](https://github.com/kelektiv/node-cron/commit/dab3d6929ca47e22388a96eb92d43258b39b093a))\n\n\n### 💎 Styles\n\n* fix linting issues ([47e665f](https://github.com/kelektiv/node-cron/commit/47e665fb176addd0eb258d5aaff85c77e7f4b17f))\n\n## [2.4.4](https://github.com/kelektiv/node-cron/compare/v2.4.3...v2.4.4) (2023-09-25)\n\n\n### 🐛 Bug Fixes\n\n* added fractional offset support ([#685](https://github.com/kelektiv/node-cron/issues/685)) ([ce78478](https://github.com/kelektiv/node-cron/commit/ce784784575b65bd75b8b1a4adda3d8fd42fe1c0))\n\n## [2.4.3](https://github.com/kelektiv/node-cron/compare/v2.4.2...v2.4.3) (2023-08-26)\n\n\n### 🐛 Bug Fixes\n\n* fix range parsing when upper limit = 0 ([#687](https://github.com/kelektiv/node-cron/issues/687)) ([d96746f](https://github.com/kelektiv/node-cron/commit/d96746f7b8f357e565d1fad48c9f70d3d646da64))\n\n\n### 🚨 Tests\n\n* add TS types check ([#690](https://github.com/kelektiv/node-cron/issues/690)) ([f046016](https://github.com/kelektiv/node-cron/commit/f046016dc64438c4a12a4615a919b046d3a846de))\n\n## [2.4.2](https://github.com/kelektiv/node-cron/compare/v2.4.1...v2.4.2) (2023-08-26)\n\n\n### 🐛 Bug Fixes\n\n* **deps:** update dependency luxon to v3.3.0 & add [@types](https://github.com/types)/luxon ([#689](https://github.com/kelektiv/node-cron/issues/689)) ([c95a449](https://github.com/kelektiv/node-cron/commit/c95a449121e440b82d391fc11f8dc148748f93ec)), closes [#688](https://github.com/kelektiv/node-cron/issues/688)\n\n## [2.4.1](https://github.com/kelektiv/node-cron/compare/v2.4.0...v2.4.1) (2023-08-14)\n\n\n### 🐛 Bug Fixes\n\n* replace loop timeout by max match date ([#686](https://github.com/kelektiv/node-cron/issues/686)) ([c685c63](https://github.com/kelektiv/node-cron/commit/c685c63a6d7fa86d6c8afca29b536b9da24e824b))\n\n\n### ⚙️ Continuous Integrations\n\n* **renovate:** configure renovate ([#683](https://github.com/kelektiv/node-cron/issues/683)) ([9dbe962](https://github.com/kelektiv/node-cron/commit/9dbe962fad1c8b1b020441bce84ab91b1a7b4415))\n\n## [2.4.0](https://github.com/kelektiv/node-cron/compare/v2.3.0...v2.4.0) (2023-07-24)\n\n\n### ✨ Features\n\n* import type definitions from [@types](https://github.com/types)/cron ([d8a2f14](https://github.com/kelektiv/node-cron/commit/d8a2f140b59f063897dd20b7bb4dc7f599d2435b))\n\n\n### 🐛 Bug Fixes\n\n* don't start job in setTime if it wasn't running ([7e26c23](https://github.com/kelektiv/node-cron/commit/7e26c23e06277bfeb04525c71b67703392dbb8b2))\n\n\n### 🛠 Builds\n\n* **npm:** ship type definitions with releases ([0b663a8](https://github.com/kelektiv/node-cron/commit/0b663a8584f87cbec63042a4c217f43f38869fc4))\n\n\n### 🚨 Tests\n\n* add test case for [#598](https://github.com/kelektiv/node-cron/issues/598) fix ([4322ef2](https://github.com/kelektiv/node-cron/commit/4322ef29fa8af201aed5cdf8b829d411311fe025))\n* don't stop/start job before using setTime ([f0d5d3f](https://github.com/kelektiv/node-cron/commit/f0d5d3f32eddb8fd77b84438fe471fd374b34566))\n\n\n### ⚙️ Continuous Integrations\n\n* add support for beta & maintenance releases ([#677](https://github.com/kelektiv/node-cron/issues/677)) ([c6fc842](https://github.com/kelektiv/node-cron/commit/c6fc8429e905b38b05ba428e0df4a0fea273614a))\n* setup conventional commits & release automation ([#673](https://github.com/kelektiv/node-cron/issues/673)) ([c6f39ff](https://github.com/kelektiv/node-cron/commit/c6f39ff384041b7f91566fc935a9b961d453dd14))\n\n\n### ♻️ Chores\n\n* update default branch name ([#678](https://github.com/kelektiv/node-cron/issues/678)) ([7471e95](https://github.com/kelektiv/node-cron/commit/7471e95cb7433b4f29cfa68da0a652ec8cf630b6))\n* wrap setTime tests in describe and move down ([31989e0](https://github.com/kelektiv/node-cron/commit/31989e06f939bf1e9dbc6c0b6fc62c0a7144b9eb))\n\n## [v2.3.1](https://github.com/kelektiv/node-cron/compare/v2.3.0...v2.3.1) (2023-05-25)\n\n### Added\n\n- Logo!\n- New test cases\n\n### Fixed\n\n- Linting issues\n\n## [v2.3.0](https://github.com/kelektiv/node-cron/compare/v2.2.0...v2.3.0) (2023-03-14)\n\n### Fixed\n\n- Some small bugs\n\n### Changed\n\n- Refactored get next date function\n\n## [v2.2.0](https://github.com/kelektiv/node-cron/compare/v2.1.0...v2.2.0) (2023-01-09)\n\n### Changed\n\n- Updated Luxon dependency\n- Updated unit tests to be compatible with new Luxon version\n\n## [v2.1.0](https://github.com/kelektiv/node-cron/compare/v2.0.0...v2.1.0) (2022-07-14)\n\n### Changed\n\n- \"Maximum iterations\" will direct the user to refer to a single canonical issue instead of creating a new one\n\n## [v2.0.0](https://github.com/kelektiv/node-cron/compare/v1.8.2...v2.0.0) (2022-05-03)\n- Upgrade vulnerable dependencies\n- Move from moment.js to luxon (breaking change)\n\n## [v1.8.2](https://github.com/kelektiv/node-cron/compare/v1.8.1...v1.8.2) (2020-01-24)\n- Fix syntax parsing bug\n\n## [v1.8.1](https://github.com/kelektiv/node-cron/compare/v1.8.0...v1.8.1) (2020-01-19)\n- Revert TS definition defaulting to DefinitelyTyped definitions.\n\n## [v1.8.0](https://github.com/kelektiv/node-cron/compare/v1.7.1...v1.8.0) (2020-01-19)\n- GH-454 - Range upper limit should default to highest value when step is provided by Noah May <noahmouse2011@gmail.com> in d36dc9581f9f68580a2016b368f8002a9f1e357d\n\n## [v1.7.1](https://github.com/kelektiv/node-cron/compare/v1.7.0...v1.7.1) (2019-04-26)\n- GH-416 - Fix issue where next execution time is incorrect in some cases in Naoya Inada <naoina@kuune.org> in c08522ff80b3987843e9930c307b76d5fe38b5dc\n\n## [v1.7.0](https://github.com/kelektiv/node-cron/compare/v1.6.0...v1.7.0) (2019-03-19)\n- GH-408 - DST issue by Shua Talansky <shua@bitbean.com> in 1e971fd6dfa6ba4b0469d99dd64e6c31189d17d3 and 849a2467d16216a9dfa818c57cc26be6b6d0899b\n\n## [v1.6.0](https://github.com/kelektiv/node-cron/compare/v1.5.1...v1.6.0) (2018-11-15)\n- GH-393, GH-394 - Remove hard limit on max iters in favor of a timeout by Nick Campbell <nicholas.j.campbell@gmail.com> in 57632b0c06c56e82f40b740b8d7986be43842735\n- GH-390 - better handling of real dates which are in the past by Nick Campbell <nicholas.j.campbell@gmail.com> in 7cbcc984aea6ec063e38829f68eb9bc0dfb1c775\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nhttps://discord.gg/yyKns29zch.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to cron <!-- omit in toc -->\n\nFirst off, thanks for taking the time to contribute! ❤️\n\nAll types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉\n\n> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:\n\n> - Star the project\n> - Refer this project in your project's readme\n> - Mention the project at local meetups and tell your friends/colleagues\n> - Share the project on forums or social medias\n> - Join the [Discord community](https://discord.gg/yyKns29zch)\n\n## Table of Contents <!-- omit in toc -->\n\n- [Code of conduct](#code-of-conduct)\n- [I Have a Question](#i-have-a-question)\n- [I Want To Contribute](#i-want-to-contribute)\n  - [Reporting Bugs](#reporting-bugs)\n  - [Suggesting Enhancements](#suggesting-enhancements)\n  - [Submitting a Pull Request](#submitting-a-pull-request)\n  - [Working With The Code](#working-with-the-code)\n- [Coding Rules](#coding-rules)\n  - [Source Code](#source-code)\n  - [Commit Messages](#commit-messages)\n- [Join The Project Team](#join-the-project-team)\n\n## Code of Conduct\n\nHelp us keep `cron` open and inclusive. Please read and follow our [Code of conduct](CODE_OF_CONDUCT.md).\n\n## I Have a Question\n\n> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/kelektiv/node-cron#readme).\n\nBefore you ask a question, it is best to search for existing [Issues](https://github.com/search?q=repo%3Akelektiv%2Fnode-cron+&type=issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.\n\nIf you then still feel the need to ask a question and need clarification, we recommend you join the [Discord community](https://discord.gg/yyKns29zch)! We will take care to answer you as soon as possible.\n\n## I Want To Contribute\n\n> ### Legal Notice <!-- omit in toc -->\n>\n> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.\n\n### Reporting Bugs\n\n#### Before Submitting a Bug Report <!-- omit in toc -->\n\nA good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.\n\n- Make sure that you are using the latest version.\n- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/kelektiv/node-cron#readme). If you are looking for support, you might want to check [this section](#i-have-a-question)).\n- Perform a [search](https://github.com/search?q=repo%3Akelektiv%2Fnode-cron++label%3Atype%3Abug&type=issues) to see if the bug/error has already been reported. If it has, add a comment to the existing issue instead of opening a new one.\n- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.\n- Collect information about the bug:\n  - Stack trace (Traceback)\n  - OS and Version\n  - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.\n  - Possibly your input and the output\n  - Can you reliably reproduce the issue? Can you also reproduce it with older versions?\n\n#### How Do I Submit a Good Bug Report? <!-- omit in toc -->\n\nWe use GitHub issues to track bugs and errors. If you run into an issue with the project:\n\n- Open a [Bug report issue](https://github.com/kelektiv/node-cron/issues/new/choose). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)\n- Explain the behavior you would expect and the actual behavior.\n- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.\n- Provide the information you collected in the previous section.\n\nOnce it's filed:\n\n- The project team will label the issue accordingly.\n- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `cannot reproduce`. Bugs with the `cannot reproduce` tag will not be addressed until they are reproduced.\n- If the team is able to reproduce the issue, it will be marked `bug`, as well as possibly other tags, and the issue will be left to be [implemented by someone](#your-first-code-contribution).\n\n### Suggesting Enhancements\n\nThis section guides you through submitting an enhancement suggestion for cron, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.\n\n#### Before Submitting an Enhancement <!-- omit in toc -->\n\n- Make sure that you are using the latest version.\n- Read the [documentation](https://github.com/kelektiv/node-cron#readme) carefully and find out if the functionality is already covered, maybe by an individual configuration.\n- Perform a [search](https://github.com/search?q=repo%3Akelektiv%2Fnode-cron++label%3Atype%3Afeature&type=issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.\n- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.\n\n#### How Do I Submit a Good Enhancement Suggestion? <!-- omit in toc -->\n\nEnhancement suggestions are tracked as [GitHub issues](https://github.com/kelektiv/node-cron/issues).\n\n- Open a [Feature request issue](https://github.com/kelektiv/node-cron/issues/new/choose).\n- Use a **clear and descriptive title** for the issue to identify the suggestion.\n- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.\n- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.\n- **Explain why this enhancement would be useful** to most cron users. You may also want to point out the other projects that solved it better and which could serve as inspiration.\n\n### Submitting a Pull Request\n\nGood pull requests, whether patches, improvements, or new features, are a fantastic help.\nThey should remain focused in scope and avoid containing unrelated commits.\n\n**Please ask first** before embarking on any significant pull requests (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's maintainers might not want to merge into the project.\n\nFor ambitious tasks, open a [**draft** Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft) as soon as possible, in order to get feedback and help from the maintainers and the community.\n\nIf you have never created a pull request before, welcome 🎉 😄.\n[Here is a great tutorial](https://opensource.guide/how-to-contribute/#opening-a-pull-request) on how to send one :)\n\nHere is a summary of the steps to follow:\n\n1. [Set up the workspace](#set-up-the-workspace)\n2. If you cloned a while ago, get the latest changes from upstream and update dependencies:\n\n```bash\n$ git checkout main\n$ git pull upstream main\n$ rm -rf node_modules\n$ nvm use\n$ npm install\n```\n\n3. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix:\n\n```bash\n$ git checkout -b <topic-branch-name>\n```\n\n4. Make your code changes, following the [Coding Rules](#coding-rules)\n5. Push your topic branch up to your fork:\n\n```bash\n$ git push origin <topic-branch-name>\n```\n\n6. [Open a Pull Request](https://help.github.com/articles/creating-a-pull-request/#creating-the-pull-request) with a clear title and description.\n\n#### Do not force push to your pull request branch\n\nPlease do not force push to your PR's branch after you have created your PR, as doing so forces us to review the whole PR again.\nThis makes it harder for us to review your work because we don't know what has changed.\nPRs will always be squashed by us when we merge your work.\nCommit as many times as you need in your pull request branch, but please batch apply review suggestions.\n\nIf you're updating your PR branch from within the GitHub PR interface, use the default \"Update branch\" button.\nThis is the \"Update with merge commit\" option in the dropdown.\n\nForce pushing a PR, or using the \"Update with rebase\" button is OK when you:\n\n- make large changes on a PR which require a full review anyway\n- bring the branch up-to-date with the target branch and incorporating the changes is more work than to create a new PR\n\n#### Apply maintainer provided review suggestions\n\nMaintainers can suggest changes while reviewing your pull request.\nTo apply these suggestions, please:\n\n1. Batch the suggestions into a logical group by selecting the **Add suggestion to batch** button\n1. Select the **Commit suggestions** button\n\nRead the [GitHub docs, Applying suggested changes](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/incorporating-feedback-in-your-pull-request#applying-suggested-changes) to learn more.\n\n#### Resolve review comments instead of commenting\n\nA maintainer can ask you to make changes, without giving you a _suggestion_ that you can apply.\nIn this case you should make the necessary changes.\n\nOnce you've done the work, resolve the conversation by selecting the **Resolve conversation** button in the PR overview.\nAvoid posting comments like \"I've done the work\", or \"Done\".\n\nRead the [GitHub Docs, resolving conversations](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#resolving-conversations) to learn more.\n\n#### Re-requesting a review\n\nPlease do not ping your reviewer(s) by mentioning them in a new comment.\nInstead, use the re-request review functionality.\nRead more about this in the [GitHub docs, Re-requesting a review](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request#re-requesting-a-review).\n\n#### Discord collaboration with maintainers\n\nThe codebase can be difficult to navigate, especially for a first-time contributor.\nWe don't want you spending an hour trying to work out something that would take us only a minute to explain.\n\nFor that reason, you'll find a `#development` channel on our [Discord community](https://discord.gg/yyKns29zch),\ndedicated to helping anyone who's working on or considering Pull Requests for `cron`.\n\n### Working With The Code\n\n#### Set up the workspace\n\n[Fork](https://docs.github.com/en/get-started/quickstart/contributing-to-projects#forking-a-repository) the project, [clone](https://docs.github.com/en/get-started/quickstart/contributing-to-projects#cloning-a-fork) your fork, configure the remotes and install the dependencies:\n\n```bash\n# Clone your fork of the repo into the current directory\n$ git clone git@github.com:<your-username>/node-cron.git # or https://github.com/<your-username>/node-cron.git for HTTPS\n\n# Navigate to the newly cloned directory\n$ cd node-cron\n\n# Assign the original repo to a remote called \"upstream\"\n$ git remote add upstream git@github.com:kelektiv/node-cron.git # or https://github.com/kelektiv/node-cron.git for HTTPS\n\n# Switch your node version to the version defined by the project as the development version\n# This step assumes you have already installed and configured https://github.com/nvm-sh/nvm\n# You may need to run `nvm install` if you have not already installed the development node version\n$ nvm use\n\n# Install the dependencies\n$ npm install\n```\n\n#### Lint\n\nThis repository uses [ESLint](https://eslint.org) and [Prettier](https://prettier.io) for linting and formatting.\n\nBefore pushing your code changes make sure there are no linting errors with `npm run lint`.\n\n**Tips**:\n\n- Most linting errors can be automatically fixed with `npm run lint:fix`.\n- Install the [ESLint plugin](https://eslint.org/docs/latest/use/integrations) for your editor to see linting errors directly in your editor and automatically fix them on save.\n\n#### Tests\n\nThis repository uses [Jest](https://jestjs.io) for writing and running tests.\n\nBefore pushing your code changes make sure all **tests pass** and the **coverage thresholds are met**:\n\n```bash\n$ npm run test\n```\n\n**Tips:**\n\n- run a single test file with `npm run test -- <file path>`, for example `npm run test -- tests/crontime.test.ts`\n- run a subset of test files with `npm run test -- <glob>`, for example `npm run test -- tests/*.test.ts`\n- run a single test case with `npm run test -- -t '<test case name regex>'`, for example `npm run test -- -t 'should parse .*'`\n- run in watch mode with `npm run test:watch` to automatically run a test case when you modify it or the associated source code (above tips also work with this command)\n\n## Coding Rules\n\n### Source Code\n\nTo ensure consistency and quality throughout the source code, all code modifications must have:\n\n- No [linting](#lint) errors\n- A [test](#tests) for every possible case introduced by your code change\n- [Valid commit message(s)](#commit-messages)\n- Documentation for new features\n- Updated documentation for modified features\n\n### Commit Messages\n\n#### Atomic commits\n\nIf possible, make [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit), which means:\n\n- a commit should contain exactly one self-contained functional change\n- a functional change should be contained in exactly one commit\n- a commit should not create an inconsistent state (such as test errors, linting errors, partial fix, feature without documentation, etc...)\n\nA complex feature can be broken down into multiple commits as long as each one maintains a consistent state and consists of a self-contained change.\n\n#### Commit message format\n\nEach commit message consists of a **header**, a **body** and a **footer**.\nThe header has a special format that includes a **type**, a **scope** and a **subject**:\n\n```commit\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\nThe **header** is mandatory and the **scope** of the header is optional.\n\nThe **footer** can contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages).\n\n#### Revert\n\nIf the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.\nIn the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.\n\n#### Type\n\nThe type must be one of the following:\n\n|    Type    | Title                    | Description                                                                                                 |\n| :--------: | ------------------------ | ----------------------------------------------------------------------------------------------------------- |\n|   `feat`   | Features                 | A new feature                                                                                               |\n|   `fix`    | Bug Fixes                | A bug Fix                                                                                                   |\n|   `docs`   | Documentation            | Documentation only changes                                                                                  |\n|  `style`   | Styles                   | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)      |\n| `refactor` | Code Refactoring         | A code change that neither fixes a bug nor adds a feature                                                   |\n|   `perf`   | Performance Improvements | A code change that improves performance                                                                     |\n|   `test`   | Tests                    | Adding missing tests or correcting existing tests                                                           |\n|  `build`   | Builds                   | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)         |\n|    `ci`    | Continuous Integrations  | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) |\n|  `chore`   | Chores                   | Other changes that don't modify src or test files                                                           |\n|  `revert`  | Reverts                  | Reverts a previous commit                                                                                   |\n\n#### Subject\n\nThe subject contains succinct description of the change:\n\n- use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- don't capitalize first letter\n- no dot (.) at the end\n\n#### Body\n\nJust as in the **subject**, use the imperative, present tense: \"change\" not \"changed\" nor \"changes\".\nThe body should include the motivation for the change and contrast this with previous behavior.\n\n#### Footer\n\nThe footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**.\n\n**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines.\nThe rest of the commit message is then used for this.\n\n#### Examples\n\n```commit\nfix(pencil): stop graphite breaking when too much pressure applied\n```\n\n```commit\nfeat(pencil): add 'graphiteWidth' option\n\nFix #42\n```\n\n```commit\nperf(pencil): remove graphiteWidth option\n\nBREAKING CHANGE: The graphiteWidth option has been removed.\n\nThe default graphite width of 10mm is always used for performance reasons.\n```\n\n## Join The Project Team\n\nThis project is looking for help! If you're interested in helping with the project on a regular basis, please reach out to us on the [Discord community](https://discord.gg/yyKns29zch).\n\n## Attribution <!-- omit in toc -->\n\nThis guide is based on the [**contributing-gen**](https://github.com/bttger/contributing-gen), [**semantic-release**'s `CONTRIBUTING.md`](https://github.com/semantic-release/semantic-release/blob/master/CONTRIBUTING.md) and [**renovate**'s `CONTRIBUTING.md`](https://github.com/renovatebot/renovate/blob/main/.github/contributing.md).\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\nCopyright © 2017 Nicholas Campbell <nicholas.j.campbell@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"logo.svg\" alt=\"cron for Node.js logo\" height=\"150\">\n  <br />\n  <b>cron</b> is a robust tool for running jobs (functions or commands) on schedules defined using the cron syntax.\n  <br />\n  Perfect for tasks like data backups, notifications, and many more!\n</p>\n\n# Cron for Node.js\n\n[![Version](https://img.shields.io/npm/v/cron?label=version&logo=npm)](https://www.npmjs.com/package/cron)\n[![Monthly Downloads](https://img.shields.io/npm/dm/cron?logo=npm)](https://www.npmjs.com/package/cron)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/kelektiv/node-cron/release.yml?logo=github)](https://github.com/kelektiv/node-cron/actions/workflows/release.yml)\n[![CodeQL Status](https://img.shields.io/github/actions/workflow/status/kelektiv/node-cron/codeql.yml?logo=github&label=CodeQL)](https://github.com/kelektiv/node-cron/actions/workflows/codeql.yml)\n[![Coverage](https://img.shields.io/codecov/c/gh/kelektiv/node-cron?logo=codecov)](https://app.codecov.io/gh/kelektiv/node-cron)\n[![Renovate](https://img.shields.io/badge/renovate-enabled-dark_green)](https://github.com/kelektiv/node-cron/issues/718)\n[![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/kelektiv/node-cron?label=openssf%20scorecard)](https://securityscorecards.dev/viewer/?uri=github.com/kelektiv/node-cron)\n[![Discord](https://img.shields.io/discord/1075597081017851934?logo=discord)](https://discord.gg/yyKns29zch)\n\n## 🌟 Features\n\n- execute a function whenever your scheduled job triggers\n- execute a job external to the javascript process (like a system command) using `child_process`\n- use a Date or Luxon DateTime object instead of cron syntax as the trigger for your callback\n- use an additional slot for seconds (leaving it off will default to 0 and match the Unix behavior)\n\n## 🚀 Installation\n\n```bash\nnpm install cron\n```\n\n## Table of Contents\n\n1. [Features](#-features)\n2. [Installation](#-installation)\n3. [Migrating](#-migrating)\n4. [Basic Usage](#-basic-usage)\n5. [Cron Patterns](#-cron-patterns)\n   - [Cron Syntax Overview](#-cron-patterns)\n   - [Supported Ranges](#supported-ranges)\n6. [API](#-api)\n   - [Standalone Functions](#standalone-functions)\n   - [CronJob Class](#cronjob-class)\n   - [CronTime Class](#crontime-class)\n7. [Gotchas](#-gotchas)\n8. [Community](#-community)\n   - [Join the Community](#-community)\n9. [Contributing](#-contributing)\n   - [General Contribution](#-contributing)\n   - [Submitting Bugs/Issues](#-submitting-bugsissues)\n10. [Acknowledgements](#-acknowledgements)\n11. [License](#-license)\n\n## ⬆ Migrating\n\nv4 dropped Node v16 and renamed the `job.running` property:\n\n<details>\n  <summary>Migrating from v3 to v4</summary>\n\n### Dropped Node version\n\nNode v16 is no longer supported. Upgrade your Node installation to Node v18 or above\n\n### Property renamed and now read-only\n\nYou can no longer set the `running` property (now `isActive`). It is read-only. To start or stop a cron job, use `job.start()` and `job.stop()`.\n\n</details>\n\nv3 introduced TypeScript and tighter Unix cron pattern alignment:\n\n<details>\n  <summary>Migrating from v2 to v3</summary>\n\n### Month & day-of-week indexing changes\n\n- **Month Indexing:** Changed from `0-11` to `1-12`. So you need to increment all numeric months by 1.\n\n- **Day-of-Week Indexing:** Support added for `7` as Sunday.\n\n### Adjustments in `CronJob`\n\n- The constructor no longer accepts an object as its first and only params. Use `CronJob.from(argsObject)` instead.\n- Callbacks are now called in the order they were registered.\n- `nextDates(count?: number)` now always returns an array (empty if no argument is provided). Use `nextDate()` instead for a single date.\n\n### Removed methods\n\n- removed `job()` method in favor of `new CronJob(...args)` / `CronJob.from(argsObject)`\n\n- removed `time()` method in favor of `new CronTime()`\n\n</details>\n\n## 🛠 Basic Usage\n\n```javascript\nimport { CronJob } from 'cron';\n\nconst job = new CronJob(\n\t'* * * * * *', // cronTime\n\tfunction () {\n\t\tconsole.log('You will see this message every second');\n\t}, // onTick\n\tnull, // onComplete\n\ttrue, // start\n\t'America/Los_Angeles' // timeZone\n);\n// job.start() is optional here because of the fourth parameter set to true.\n```\n\n```javascript\n// equivalent job using the \"from\" static method, providing parameters as an object\nconst job = CronJob.from({\n\tcronTime: '* * * * * *',\n\tonTick: function () {\n\t\tconsole.log('You will see this message every second');\n\t},\n\tstart: true,\n\ttimeZone: 'America/Los_Angeles'\n});\n```\n\n> **Note:** In the first example above, the fourth parameter to `CronJob()` starts the job automatically. If not provided or set to falsy, you must explicitly start the job using `job.start()`.\n\nFor more advanced examples, check the [examples directory](https://github.com/kelektiv/node-cron/tree/main/examples).\n\n## ⏰ Cron Patterns\n\nCron patterns are the backbone of this library. Familiarize yourself with the syntax:\n\n```\n- `*` Asterisks: Any value\n- `1-3,5` Ranges: Ranges and individual values\n- `*/2` Steps: Every two units\n```\n\nDetailed patterns and explanations are available at [crontab.org](http://crontab.org). The examples in the link have five fields, and 1 minute as the finest granularity, but our cron scheduling supports an enhanced format with six fields, allowing for second-level precision. Tools like [crontab.guru](https://crontab.guru/) can help in constructing patterns but remember to account for the seconds field.\n\n### Supported Ranges\n\nHere's a quick reference to the UNIX Cron format this library uses, plus an added second field:\n\n```\nfield          allowed values\n-----          --------------\nsecond         0-59\nminute         0-59\nhour           0-23\nday of month   1-31\nmonth          1-12 (or names, see below)\nday of week    0-7 (0 or 7 is Sunday, or use names)\n```\n\n> Names can also be used for the 'month' and 'day of week' fields. Use the first three letters of the particular day or month (case does not matter). Ranges and lists of names are allowed.  \n> Examples: \"mon,wed,fri\", \"jan-mar\".\n\n## 📖 API\n\n### Standalone Functions\n\n- `sendAt`: Indicates when a `CronTime` will execute (returns a Luxon `DateTime` object).\n\n  ```javascript\n  import * as cron from 'cron';\n\n  const dt = cron.sendAt('0 0 * * *');\n  console.log(`The job would run at: ${dt.toISO()}`);\n  ```\n\n- `timeout`: Indicates the number of milliseconds in the future at which a `CronTime` will execute (returns a number).\n\n  ```javascript\n  import * as cron from 'cron';\n\n  const timeout = cron.timeout('0 0 * * *');\n  console.log(`The job would run in ${timeout}ms`);\n  ```\n\n- `validateCronExpression`: Validates if a given cron expression is valid (returns an object with `valid` and `error` properties).\n\n  ```javascript\n  import * as cron from 'cron';\n\n  const validation = cron.validateCronExpression('0 0 * * *');\n  console.log(`Is the cron expression valid? ${validation.valid}`);\n  if (!validation.valid) {\n  \tconsole.error(`Validation error: ${validation.error}`);\n  }\n  ```\n\n### CronJob Class\n\n#### Constructor\n\n`constructor(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, unrefTimeout, waitForCompletion, errorHandler, name, threshold)`:\n\n- `cronTime`: [REQUIRED] - The time to fire off your job. Can be cron syntax, a JS [`Date`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date) object or a Luxon [`DateTime`](https://moment.github.io/luxon/api-docs/index.html#datetime) object.\n\n- `onTick`: [REQUIRED] - Function to execute at the specified time. If an `onComplete` callback was provided, `onTick` will receive it as an argument.\n\n- `onComplete`: [OPTIONAL] - Invoked when the job is halted with `job.stop()`. It might also be triggered by `onTick` post its run.\n\n- `start`: [OPTIONAL] - Determines if the job should commence before constructor exit. Default is `false`.\n\n- `timeZone`: [OPTIONAL] - Sets the execution time zone. Default is local time. Check valid formats in the [Luxon documentation](https://github.com/moment/luxon/blob/master/docs/zones.md#specifying-a-zone).\n\n- `context`: [OPTIONAL] - Execution context for the onTick method.\n\n- `runOnInit`: [OPTIONAL] - Instantly triggers the `onTick` function post initialization. Default is `false`.\n\n- `utcOffset`: [OPTIONAL] - Specifies time zone offset in minutes. Cannot co-exist with `timeZone`.\n\n- `unrefTimeout`: [OPTIONAL] - Useful for controlling event loop behavior. More details [here](https://nodejs.org/api/timers.html#timers_timeout_unref).\n\n- `waitForCompletion`: [OPTIONAL] - If `true`, no additional instances of the `onTick` callback function will run until the current onTick callback has completed. Any new scheduled executions that occur while the current callback is running will be skipped entirely. Default is `false`.\n\n- `errorHandler`: [OPTIONAL] - Function to handle any exceptions that occur in the `onTick` method.\n\n- `name`: [OPTIONAL] - Name of the job. Useful for identifying jobs in logs.\n\n- `threshold`: [OPTIONAL] - Threshold in ms to control whether to execute or skip missed execution deadlines caused by slow or busy hardware. Execution delays within threshold will be executed immediately, and otherwise will be skipped. In both cases a warning will be printed to the console with the job name and cron expression. See [issue #962](https://github.com/kelektiv/node-cron/issues/962) for more information. Default is `250`.\n\n#### Methods\n\n- `from` (static): Create a new CronJob object providing arguments as an object. See argument names and descriptions above.\n\n- `start`: Initiates the job.\n\n- `stop`: Halts the job.\n\n- `setTime`: Modifies the time for the `CronJob`. Parameter must be a `CronTime`.\n\n- `lastDate`: Provides the last execution date.\n\n- `nextDate`: Indicates the subsequent date that will activate an `onTick`.\n\n- `nextDates(count)`: Supplies an array of upcoming dates that will initiate an `onTick`.\n\n- `fireOnTick`: Allows modification of the `onTick` calling behavior.\n\n- `addCallback`: Permits addition of `onTick` callbacks.\n\n#### Properties\n\n- `isActive`: [READ-ONLY] Indicates if a job is active (checking to see if the callback needs to be called).\n\n- `isCallbackRunning`: [READ-ONLY] Indicates if a callback is currently executing.\n\n  ```javascript\n  const job = new CronJob('* * * * * *', async () => {\n  \tconsole.log(job.isCallbackRunning); // true during callback execution\n  \tawait someAsyncTask();\n  \tconsole.log(job.isCallbackRunning); // still true until callback completes\n  });\n\n  console.log(job.isCallbackRunning); // false\n  job.start();\n  console.log(job.isActive); // true\n  console.log(job.isCallbackRunning); // false\n  ```\n\n### CronTime Class\n\n#### Constructor\n\n`constructor(time, zone, utcOffset)`:\n\n- `time`: [REQUIRED] - The time to initiate your job. Accepts cron syntax or a JS [Date](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date) object.\n\n- `zone`: [OPTIONAL] - Equivalent to `timeZone` from `CronJob` parameters.\n\n- `utcOffset`: [OPTIONAL] - Analogous to `utcOffset` from `CronJob` parameters.\n\n## 💢 Gotchas\n\n- Both JS `Date` and Luxon `DateTime` objects don't guarantee millisecond precision due to computation delays. This module excludes millisecond precision for standard cron syntax but allows execution date specification through JS `Date` or Luxon `DateTime` objects. However, specifying a precise future execution time, such as adding a millisecond to the current time, may not always work due to these computation delays. It's observed that delays less than 4-5 ms might lead to inconsistencies. While we could limit all date granularity to seconds, we've chosen to allow greater precision but advise users of potential issues.\n\n- Using arrow functions for `onTick` binds them to the parent's `this` context. As a result, they won't have access to the cronjob's `this` context. You can read a little more in issue [#47 (comment)](https://github.com/kelektiv/node-cron/issues/47#issuecomment-459762775).\n\n## 🤝 Community\n\nJoin the [Discord server](https://discord.gg/yyKns29zch)! Here you can discuss issues and get help in a more casual forum than GitHub.\n\n## 🌍 Contributing\n\nThis project is looking for help! If you're interested in helping with the project, please take a look at our [contributing documentation](https://github.com/kelektiv/node-cron/blob/main/CONTRIBUTING.md).\n\n### 🐛 Submitting Bugs/Issues\n\nPlease have a look at our [contributing documentation](https://github.com/kelektiv/node-cron/blob/main/CONTRIBUTING.md), it contains all the information you need to know before submitting an issue.\n\n## 🙏 Acknowledgements\n\nThis is a community effort project. In the truest sense, this project started as an open source project from [cron.js](http://github.com/padolsey/cron.js) and grew into something else. Other people have contributed code, time, and oversight to the project. At this point there are too many to name here so we'll just say thanks.\n\nSpecial thanks to [Hiroki Horiuchi](https://github.com/horiuchi), [Lundarl Gholoi](https://github.com/winup) and [koooge](https://github.com/koooge) for their work on the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) typings before they were imported in v2.4.0.\n\n## ⚖ License\n\nMIT\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport tseslint from 'typescript-eslint';\nimport globals from 'globals';\nimport tsParser from '@typescript-eslint/parser';\nimport jest from 'eslint-plugin-jest';\nimport js from '@eslint/js';\n\nexport default [\n\t// using .mjs for module support as TypeScript Eslint configs are still experimental\n\t{\n\t\tname: 'Eslint config file',\n\t\tfiles: ['eslint.config.mjs'],\n\t\t// ...tseslint.configs.disableTypeChecked,\n\t\t// ...js.configs.recommended,\n\n\t\tlanguageOptions: {\n\t\t\tsourceType: 'module',\n\t\t\tparserOptions: {\n\t\t\t\t// project: 'package.json'\n\t\t\t}\n\t\t}\n\t},\n\n\t{\n\t\tname: 'general project rules',\n\t\tfiles: ['**/*.ts'],\n\n\t\tplugins: {\n\t\t\t'@typescript-eslint': tseslint.plugin\n\t\t},\n\n\t\tlanguageOptions: {\n\t\t\tglobals: {\n\t\t\t\t...globals.node\n\t\t\t},\n\n\t\t\tparser: tsParser,\n\t\t\tecmaVersion: 5,\n\t\t\tsourceType: 'module',\n\n\t\t\tparserOptions: {\n\t\t\t\tproject: 'tsconfig.json'\n\t\t\t}\n\t\t},\n\n\t\trules: {\n\t\t\t// contains all of recommended, recommended-type-checked, and strict\n\t\t\t...tseslint.configs.strictTypeChecked.rules,\n\t\t\t'@typescript-eslint/no-unused-vars': [\n\t\t\t\t'warn',\n\t\t\t\t{\n\t\t\t\t\targsIgnorePattern: '^_'\n\t\t\t\t}\n\t\t\t],\n\n\t\t\t'@typescript-eslint/naming-convention': [\n\t\t\t\t'warn',\n\t\t\t\t{\n\t\t\t\t\tselector: ['typeLike'],\n\t\t\t\t\tformat: ['PascalCase']\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: ['variableLike', 'function'],\n\t\t\t\t\tformat: ['camelCase'],\n\t\t\t\t\tleadingUnderscore: 'allow'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: ['variable'],\n\t\t\t\t\tformat: ['camelCase', 'UPPER_CASE']\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: 'variable',\n\t\t\t\t\ttypes: ['boolean'],\n\t\t\t\t\tformat: ['PascalCase'],\n\t\t\t\t\tprefix: ['is', 'should', 'has', 'can', 'did', 'was', 'will']\n\t\t\t\t}\n\t\t\t],\n\t\t\t// this is set to warn because it wasn't caught before the eslint migration in 11/2024\n\t\t\t'@typescript-eslint/restrict-template-expressions': 'warn',\n\t\t\t'capitalized-comments': ['error', 'never']\n\t\t}\n\t},\n\n\t{\n\t\tname: 'test rules',\n\t\tfiles: ['tests/**/*.ts'],\n\n\t\t...jest.configs['flat/recommended'],\n\t\t...jest.configs['flat/style'],\n\n\t\tplugins: {\n\t\t\tjest\n\t\t},\n\n\t\trules: {\n\t\t\t'@typescript-eslint/no-empty-function': 'off',\n\t\t\t'@typescript-eslint/unbound-method': 'off',\n\t\t\t'jest/no-done-callback': 'off'\n\t\t}\n\t},\n\n\t// Prettier plugin which should be last\n\teslintPluginPrettierRecommended\n];\n"
  },
  {
    "path": "examples/at_10_minutes.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('* 10 * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('At Ten Minutes:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/at_midnight.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('00 00 00 * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Midnight:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/basic.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('* * * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Every second:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/complex_expr.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('* 4-22 * * 1-5', function () {\n\tconst d = new Date();\n\tconsole.log('Every Minute Between hours 4-22, Monday through Friday:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/every_10_minutes.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('0 */10 * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Every Tenth Minute:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/every_30_minutes_between_9_and_5.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('0 */30 9-17 * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Every 30 minutes between 9-17:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/get_next_runs.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconst job = new CronJob(\n\t'0 * * * * *',\n\tfunction () {\n\t\tconsole.log('Date: ', new Date());\n\t},\n\tnull,\n\ttrue\n);\n\nconsole.log('System TZ next 5: ', job.nextDates(5));\n\nconst jobUTC = new CronJob(\n\t'0 * * * * *',\n\tfunction () {\n\t\tconsole.log('Date: ', new Date());\n\t},\n\tnull,\n\ttrue,\n\t'UTC'\n);\n\nconsole.log('UTC next 5: ', jobUTC.nextDates(5));\n"
  },
  {
    "path": "examples/in_the_past.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\n// XXX: SEE README GOTCHAS ABOUT WHY THIS COULD BE IN THE PAST!\nconst d = new Date();\nd.setMilliseconds(d.getMilliseconds() + 1);\n\nconsole.log('Before job instantiation');\nconst job = new CronJob(\n\td,\n\t() => {\n\t\tconst d2 = new Date();\n\t\tconsole.log('Tick @:', d2);\n\t},\n\t() => {\n\t\tconsole.log('complete');\n\t}\n);\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/is_crontime_valid.mjs",
    "content": "import { validateCronExpression } from '../dist/index.js';\n\nconsole.log('is valid? ', validateCronExpression('NOT VALID').valid);\n"
  },
  {
    "path": "examples/is_job_running.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('* * * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Every second:', d);\n});\nconsole.log('After job instantiation');\njob.start();\nconsole.log('is job active? ', job.isActive);\n"
  },
  {
    "path": "examples/long_running_on_tick.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nlet isRunning = false;\nconsole.log('Before job instantiation');\nconst job = new CronJob('* * * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Check every second:', d, ', isRunning: ', isRunning);\n\n\tif (!isRunning) {\n\t\tisRunning = true;\n\n\t\tsetTimeout(function () {\n\t\t\tconsole.log('Long running onTick complete:', new Date());\n\t\t\tisRunning = false;\n\t\t}, 3000);\n\t\tconsole.log('setTimeout triggered:', new Date());\n\t}\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/mon_to_fri_at_11_30.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('00 30 11 * * 1-5', function () {\n\tconst d = new Date();\n\tconsole.log('onTick:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/multiple_jobs.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst job = new CronJob('*/5 * * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('First:', d);\n});\n\nconst job2 = new CronJob('*/8 * * * * *', function () {\n\tconst d = new Date();\n\tconsole.log('Second:', d);\n});\nconsole.log('After job instantiation');\njob.start();\njob2.start();\n"
  },
  {
    "path": "examples/object_param.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nlet isRunning = false;\nconsole.log('Before job instantiation');\nconst job = CronJob.from({\n\tcronTime: '* * * * * *',\n\tonTick: function () {\n\t\tconst d = new Date();\n\t\tconsole.log('Check every second:', d, ', isRunning: ', isRunning);\n\n\t\tif (!isRunning) {\n\t\t\tisRunning = true;\n\n\t\t\tsetTimeout(function () {\n\t\t\t\tconsole.log('Long running onTick complete:', new Date());\n\t\t\t\tisRunning = false;\n\t\t\t}, 3000);\n\t\t\tconsole.log('setTimeout triggered:', new Date());\n\t\t}\n\t}\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/run_at_specific_date.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('Before job instantiation');\nconst date = new Date();\ndate.setSeconds(date.getSeconds() + 2);\nconst job = new CronJob(date, function () {\n\tconst d = new Date();\n\tconsole.log('Specific date:', date, ', onTick at:', d);\n});\nconsole.log('After job instantiation');\njob.start();\n"
  },
  {
    "path": "examples/time_dom_syntax_with_tz.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('first');\nconst job = new CronJob(\n\t'0 0 9 4 * *',\n\tfunction () {\n\t\tconsole.log('message');\n\t},\n\tnull,\n\ttrue,\n\t'America/Sao_Paulo'\n);\nconsole.log('second');\n"
  },
  {
    "path": "examples/utc_offset_syntax.mjs",
    "content": "import { CronJob } from '../dist/index.js';\n\nconsole.log('first');\nconst job = new CronJob(\n\t'0 0 9 4 * *',\n\tfunction () {\n\t\tconsole.log('message');\n\t},\n\tnull,\n\ttrue,\n\tnull,\n\tnull,\n\tnull,\n\t-360 // UTC-6, represented in minutes\n);\nconsole.log('second');\n"
  },
  {
    "path": "jest.config.json",
    "content": "{\n\t\"testEnvironment\": \"node\",\n\t\"transform\": {\n\t\t\"^.+\\\\.(t|j)sx?$\": \"@swc/jest\"\n\t},\n\t\"collectCoverage\": true,\n\t\"coverageThreshold\": {\n\t\t\"global\": {\n\t\t\t\"statements\": 85,\n\t\t\t\"branches\": 75,\n\t\t\t\"functions\": 85,\n\t\t\t\"lines\": 85\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"cron\",\n\t\"description\": \"Cron jobs for your node\",\n\t\"version\": \"4.4.0\",\n\t\"author\": \"Brandon der Blätter <https://intcreator.com/> (https://github.com/intcreator)\",\n\t\"funding\": {\n\t\t\"type\": \"ko-fi\",\n\t\t\"url\": \"https://ko-fi.com/intcreator\"\n\t},\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/kelektiv/node-cron/issues\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/kelektiv/node-cron.git\"\n\t},\n\t\"main\": \"dist/index.js\",\n\t\"types\": \"dist/index.d.ts\",\n\t\"scripts\": {\n\t\t\"build\": \"tsc -b tsconfig.build.json\",\n\t\t\"lint:eslint\": \"eslint src/ tests/\",\n\t\t\"lint:prettier\": \"prettier ./**/*.{json,md,yml} --check\",\n\t\t\"lint\": \"npm run lint:eslint && npm run lint:prettier\",\n\t\t\"lint:fix\": \"npm run lint:eslint -- --fix && npm run lint:prettier -- --write\",\n\t\t\"test\": \"cross-env TZ='Europe/Paris' jest --coverage\",\n\t\t\"test:watch\": \"cross-env TZ='Europe/Paris' jest --watch --coverage\",\n\t\t\"test:fuzz\": \"cross-env TZ='Europe/Paris' jest --testMatch='**/*.fuzz.ts' --coverage=false --testTimeout=120000\",\n\t\t\"prepare\": \"husky\"\n\t},\n\t\"dependencies\": {\n\t\t\"@types/luxon\": \"~3.7.0\",\n\t\t\"luxon\": \"~3.7.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@commitlint/cli\": \"20.4.3\",\n\t\t\"@eslint/js\": \"9.39.4\",\n\t\t\"@fast-check/jest\": \"2.1.1\",\n\t\t\"@insurgent/commitlint-config\": \"20.0.0\",\n\t\t\"@insurgent/conventional-changelog-preset\": \"10.0.0\",\n\t\t\"@semantic-release/changelog\": \"6.0.3\",\n\t\t\"@semantic-release/commit-analyzer\": \"13.0.1\",\n\t\t\"@semantic-release/git\": \"10.0.1\",\n\t\t\"@semantic-release/github\": \"12.0.6\",\n\t\t\"@semantic-release/npm\": \"13.1.5\",\n\t\t\"@semantic-release/release-notes-generator\": \"14.1.0\",\n\t\t\"@swc/core\": \"1.15.18\",\n\t\t\"@swc/jest\": \"0.2.39\",\n\t\t\"@types/jest\": \"30.0.0\",\n\t\t\"@types/node\": \"24.10.4\",\n\t\t\"@types/sinon\": \"21.0.0\",\n\t\t\"cross-env\": \"7.0.3\",\n\t\t\"eslint\": \"8.57.1\",\n\t\t\"eslint-config-prettier\": \"9.1.2\",\n\t\t\"eslint-plugin-jest\": \"27.9.0\",\n\t\t\"eslint-plugin-prettier\": \"5.5.5\",\n\t\t\"husky\": \"9.1.7\",\n\t\t\"jest\": \"30.2.0\",\n\t\t\"lint-staged\": \"15.5.2\",\n\t\t\"prettier\": \"3.8.1\",\n\t\t\"semantic-release\": \"25.0.3\",\n\t\t\"sinon\": \"21.0.2\",\n\t\t\"typescript\": \"5.9.3\",\n\t\t\"typescript-eslint\": \"7.18.0\"\n\t},\n\t\"keywords\": [\n\t\t\"cron\",\n\t\t\"node cron\",\n\t\t\"node-cron\",\n\t\t\"schedule\",\n\t\t\"scheduler\",\n\t\t\"cronjob\",\n\t\t\"cron job\"\n\t],\n\t\"license\": \"MIT\",\n\t\"contributors\": [\n\t\t\"Nick Campbell <nicholas.j.campbell@gmail.com> (https://github.com/ncb000gt)\",\n\t\t\"Pierre Cavin <me@sherlox.io> (https://github.com/sheerlox)\",\n\t\t\"Romain Beauxis <toots@rastageeks.org> (https://github.com/toots)\",\n\t\t\"James Padolsey <> (https://github.com/jamespadolsey)\",\n\t\t\"Finn Herpich <fh@three-heads.de> (https://github.com/ErrorProne)\",\n\t\t\"Clifton Cunningham <clifton.cunningham@gmail.com> (https://github.com/cliftonc)\",\n\t\t\"Eric Abouaf <eric.abouaf@gmail.com> (https://github.com/neyric)\",\n\t\t\"humanchimp <morphcham@gmail.com> (https://github.com/humanchimp)\",\n\t\t\"Craig Condon <craig@spiceapps.com> (https://github.com/spiceapps)\",\n\t\t\"Dan Bear <daniel@hulu.com> (https://github.com/danhbear)\",\n\t\t\"Vadim Baryshev <vadimbaryshev@gmail.com> (https://github.com/baryshev)\",\n\t\t\"Leandro Ferrari <lfthomaz@gmail.com> (https://github.com/lfthomaz)\",\n\t\t\"Gregg Zigler <greggzigler@gmail.com> (https://github.com/greggzigler)\",\n\t\t\"Jordan Abderrachid <jabderrachid@gmail.com> (https://github.com/jordanabderrachid)\",\n\t\t\"Masakazu Matsushita <matsukaz@gmail.com> (matsukaz)\",\n\t\t\"Christopher Lunt <me@kirisu.co.uk> (https://github.com/kirisu)\"\n\t],\n\t\"engines\": {\n\t\t\"node\": \">=18.x\"\n\t},\n\t\"files\": [\n\t\t\"dist/**/*.js\",\n\t\t\"dist/**/*.d.ts\",\n\t\t\"CHANGELOG.md\",\n\t\t\"LICENSE\",\n\t\t\"README.md\"\n\t],\n\t\"lint-staged\": {\n\t\t\"*.ts\": \"eslint src/ tests/ --fix\",\n\t\t\"*.{json,md,yml}\": \"prettier ./**/*.{json,md,yml} --check --write\"\n\t}\n}\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export const CONSTRAINTS = Object.freeze({\n\tsecond: [0, 59],\n\tminute: [0, 59],\n\thour: [0, 23],\n\tdayOfMonth: [1, 31],\n\tmonth: [1, 12],\n\tdayOfWeek: [0, 7]\n} as const);\nexport const PARSE_DEFAULTS = Object.freeze({\n\tsecond: '0',\n\tminute: '*',\n\thour: '*',\n\tdayOfMonth: '*',\n\tmonth: '*',\n\tdayOfWeek: '*'\n} as const);\nexport const ALIASES = Object.freeze({\n\tjan: 1,\n\tfeb: 2,\n\tmar: 3,\n\tapr: 4,\n\tmay: 5,\n\tjun: 6,\n\tjul: 7,\n\taug: 8,\n\tsep: 9,\n\toct: 10,\n\tnov: 11,\n\tdec: 12,\n\tsun: 0,\n\tmon: 1,\n\ttue: 2,\n\twed: 3,\n\tthu: 4,\n\tfri: 5,\n\tsat: 6\n} as const);\nexport const TIME_UNITS_MAP = Object.freeze({\n\tSECOND: 'second',\n\tMINUTE: 'minute',\n\tHOUR: 'hour',\n\tDAY_OF_MONTH: 'dayOfMonth',\n\tMONTH: 'month',\n\tDAY_OF_WEEK: 'dayOfWeek'\n} as const);\nexport const TIME_UNITS = Object.freeze(Object.values(TIME_UNITS_MAP)) as [\n\t'second',\n\t'minute',\n\t'hour',\n\t'dayOfMonth',\n\t'month',\n\t'dayOfWeek'\n];\nexport const TIME_UNITS_LEN: number = TIME_UNITS.length;\nexport const PRESETS = Object.freeze({\n\t'@yearly': '0 0 0 1 1 *',\n\t'@monthly': '0 0 0 1 * *',\n\t'@weekly': '0 0 0 * * 0',\n\t'@daily': '0 0 0 * * *',\n\t'@hourly': '0 0 * * * *',\n\t'@minutely': '0 * * * * *',\n\t'@secondly': '* * * * * *',\n\t'@weekdays': '0 0 0 * * 1-5',\n\t'@weekends': '0 0 0 * * 0,6'\n} as const);\nexport const RE_WILDCARDS = /\\*/g;\nexport const RE_RANGE = /^(\\d+)(?:-(\\d+))?(?:\\/(\\d+))?$/g;\n"
  },
  {
    "path": "src/errors.ts",
    "content": "export class CronError extends Error {}\n\nexport class ExclusiveParametersError extends CronError {\n\tconstructor(param1: string, param2: string) {\n\t\tsuper(`You can't specify both ${param1} and ${param2}`);\n\t}\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { DateTime } from 'luxon';\nimport { CronTime } from './time';\n\nexport { CronJob } from './job';\nexport { CronTime } from './time';\n\nexport {\n\tCronCallback,\n\tCronCommand,\n\tCronContext,\n\tCronJobParams,\n\tCronOnCompleteCallback,\n\tCronOnCompleteCommand,\n\tRanges,\n\tTimeUnit\n} from './types/cron.types';\n\nexport const sendAt = (cronTime: string | Date | DateTime): DateTime =>\n\tnew CronTime(cronTime).sendAt();\n\nexport const timeout = (cronTime: string | Date | DateTime): number =>\n\tnew CronTime(cronTime).getTimeout();\n\nexport const validateCronExpression = CronTime.validateCronExpression;\n"
  },
  {
    "path": "src/job.ts",
    "content": "import { spawn } from 'child_process';\nimport { CronError, ExclusiveParametersError } from './errors';\nimport { CronTime } from './time';\nimport {\n\tCronCallback,\n\tCronCommand,\n\tCronContext,\n\tCronJobParams,\n\tCronOnCompleteCallback,\n\tCronOnCompleteCommand,\n\tWithOnComplete\n} from './types/cron.types';\n\nexport class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {\n\tcronTime: CronTime;\n\tunrefTimeout = false;\n\tlastExecution: Date | null = null;\n\trunOnce = false;\n\tcontext: CronContext<C>;\n\tonComplete?: WithOnComplete<OC> extends true\n\t\t? CronOnCompleteCallback\n\t\t: undefined;\n\twaitForCompletion = false;\n\terrorHandler?: CronJobParams<OC, C>['errorHandler'];\n\tname?: string; // optional job name for identification\n\tthreshold = 250; // default threshold in ms\n\n\tprivate _isActive = false;\n\tprivate _isCallbackRunning = false;\n\tprivate _timeout?: NodeJS.Timeout;\n\tprivate _callbacks: CronCallback<C, WithOnComplete<OC>>[] = [];\n\n\tget isActive() {\n\t\treturn this._isActive;\n\t}\n\n\tget isCallbackRunning() {\n\t\treturn this._isCallbackRunning;\n\t}\n\n\tconstructor(\n\t\tcronTime: CronJobParams<OC, C>['cronTime'],\n\t\tonTick: CronJobParams<OC, C>['onTick'],\n\t\tonComplete?: CronJobParams<OC, C>['onComplete'],\n\t\tstart?: CronJobParams<OC, C>['start'],\n\t\ttimeZone?: CronJobParams<OC, C>['timeZone'],\n\t\tcontext?: CronJobParams<OC, C>['context'],\n\t\trunOnInit?: CronJobParams<OC, C>['runOnInit'],\n\t\tutcOffset?: null,\n\t\tunrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],\n\t\twaitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],\n\t\terrorHandler?: CronJobParams<OC, C>['errorHandler'],\n\t\tname?: CronJobParams<OC, C>['name'],\n\t\tthreshold?: CronJobParams<OC, C>['threshold']\n\t);\n\tconstructor(\n\t\tcronTime: CronJobParams<OC, C>['cronTime'],\n\t\tonTick: CronJobParams<OC, C>['onTick'],\n\t\tonComplete?: CronJobParams<OC, C>['onComplete'],\n\t\tstart?: CronJobParams<OC, C>['start'],\n\t\ttimeZone?: null,\n\t\tcontext?: CronJobParams<OC, C>['context'],\n\t\trunOnInit?: CronJobParams<OC, C>['runOnInit'],\n\t\tutcOffset?: CronJobParams<OC, C>['utcOffset'],\n\t\tunrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],\n\t\twaitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],\n\t\terrorHandler?: CronJobParams<OC, C>['errorHandler'],\n\t\tname?: CronJobParams<OC, C>['name'],\n\t\tthreshold?: CronJobParams<OC, C>['threshold']\n\t);\n\tconstructor(\n\t\tcronTime: CronJobParams<OC, C>['cronTime'],\n\t\tonTick: CronJobParams<OC, C>['onTick'],\n\t\tonComplete?: CronJobParams<OC, C>['onComplete'],\n\t\tstart?: CronJobParams<OC, C>['start'],\n\t\ttimeZone?: CronJobParams<OC, C>['timeZone'],\n\t\tcontext?: CronJobParams<OC, C>['context'],\n\t\trunOnInit?: CronJobParams<OC, C>['runOnInit'],\n\t\tutcOffset?: CronJobParams<OC, C>['utcOffset'],\n\t\tunrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],\n\t\twaitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],\n\t\terrorHandler?: CronJobParams<OC, C>['errorHandler'],\n\t\tname?: CronJobParams<OC, C>['name'],\n\t\tthreshold?: CronJobParams<OC, C>['threshold']\n\t) {\n\t\tthis.context = (context ?? this) as CronContext<C>;\n\t\tthis.waitForCompletion = Boolean(waitForCompletion);\n\n\t\tthis.errorHandler = errorHandler;\n\n\t\t// runtime check for JS users\n\t\tif (timeZone != null && utcOffset != null) {\n\t\t\tthrow new ExclusiveParametersError('timeZone', 'utcOffset');\n\t\t}\n\n\t\tif (timeZone != null) {\n\t\t\tthis.cronTime = new CronTime(cronTime, timeZone, null);\n\t\t} else if (utcOffset != null) {\n\t\t\tthis.cronTime = new CronTime(cronTime, null, utcOffset);\n\t\t} else {\n\t\t\tthis.cronTime = new CronTime(cronTime, timeZone, utcOffset);\n\t\t}\n\n\t\tif (unrefTimeout != null) {\n\t\t\tthis.unrefTimeout = unrefTimeout;\n\t\t}\n\n\t\tif (onComplete != null) {\n\t\t\t// casting to the correct type since we just made sure that WithOnComplete<OC> = true\n\t\t\tthis.onComplete = this._fnWrap(\n\t\t\t\tonComplete\n\t\t\t) as WithOnComplete<OC> extends true ? CronOnCompleteCallback : undefined;\n\t\t}\n\n\t\tif (threshold != null) {\n\t\t\tthis.threshold = Math.abs(threshold);\n\t\t}\n\n\t\tif (name != null) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\tif (this.cronTime.realDate) {\n\t\t\tthis.runOnce = true;\n\t\t}\n\n\t\tthis.addCallback(this._fnWrap(onTick));\n\n\t\tif (runOnInit) {\n\t\t\tthis.lastExecution = new Date();\n\t\t\tvoid this.fireOnTick();\n\t\t}\n\n\t\tif (start) this.start();\n\t}\n\n\tstatic from<OC extends CronOnCompleteCommand | null = null, C = null>(\n\t\tparams: CronJobParams<OC, C>\n\t) {\n\t\t// runtime check for JS users\n\t\t// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n\t\tif (params.timeZone != null && params.utcOffset != null) {\n\t\t\tthrow new ExclusiveParametersError('timeZone', 'utcOffset');\n\t\t}\n\n\t\tif (params.timeZone != null) {\n\t\t\treturn new CronJob<OC, C>(\n\t\t\t\tparams.cronTime,\n\t\t\t\tparams.onTick,\n\t\t\t\tparams.onComplete,\n\t\t\t\tparams.start,\n\t\t\t\tparams.timeZone,\n\t\t\t\tparams.context,\n\t\t\t\tparams.runOnInit,\n\t\t\t\tparams.utcOffset,\n\t\t\t\tparams.unrefTimeout,\n\t\t\t\tparams.waitForCompletion,\n\t\t\t\tparams.errorHandler,\n\t\t\t\tparams.name,\n\t\t\t\tparams.threshold\n\t\t\t);\n\t\t} else if (params.utcOffset != null) {\n\t\t\treturn new CronJob<OC, C>(\n\t\t\t\tparams.cronTime,\n\t\t\t\tparams.onTick,\n\t\t\t\tparams.onComplete,\n\t\t\t\tparams.start,\n\t\t\t\tnull,\n\t\t\t\tparams.context,\n\t\t\t\tparams.runOnInit,\n\t\t\t\tparams.utcOffset,\n\t\t\t\tparams.unrefTimeout,\n\t\t\t\tparams.waitForCompletion,\n\t\t\t\tparams.errorHandler,\n\t\t\t\tparams.name,\n\t\t\t\tparams.threshold\n\t\t\t);\n\t\t} else {\n\t\t\treturn new CronJob<OC, C>(\n\t\t\t\tparams.cronTime,\n\t\t\t\tparams.onTick,\n\t\t\t\tparams.onComplete,\n\t\t\t\tparams.start,\n\t\t\t\tparams.timeZone,\n\t\t\t\tparams.context,\n\t\t\t\tparams.runOnInit,\n\t\t\t\tparams.utcOffset,\n\t\t\t\tparams.unrefTimeout,\n\t\t\t\tparams.waitForCompletion,\n\t\t\t\tparams.errorHandler,\n\t\t\t\tparams.name,\n\t\t\t\tparams.threshold\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate _fnWrap(cmd: CronCommand<C, boolean>): CronCallback<C, boolean> {\n\t\tswitch (typeof cmd) {\n\t\t\tcase 'function': {\n\t\t\t\treturn cmd;\n\t\t\t}\n\n\t\t\tcase 'string': {\n\t\t\t\tconst [command, ...args] = cmd.split(' ');\n\n\t\t\t\treturn spawn.bind(undefined, command ?? cmd, args, {}) as () => void;\n\t\t\t}\n\n\t\t\tcase 'object': {\n\t\t\t\treturn spawn.bind(\n\t\t\t\t\tundefined,\n\t\t\t\t\tcmd.command,\n\t\t\t\t\tcmd.args ?? [],\n\t\t\t\t\tcmd.options ?? {}\n\t\t\t\t) as () => void;\n\t\t\t}\n\t\t}\n\t}\n\n\taddCallback(callback: CronCallback<C, WithOnComplete<OC>>) {\n\t\tif (typeof callback === 'function') {\n\t\t\tthis._callbacks.push(callback);\n\t\t}\n\t}\n\n\tsetTime(time: CronTime) {\n\t\tif (!(time instanceof CronTime)) {\n\t\t\tthrow new CronError('time must be an instance of CronTime.');\n\t\t}\n\n\t\tconst wasRunning = this._isActive;\n\t\tthis.stop();\n\n\t\tthis.cronTime = time;\n\t\tif (time.realDate) this.runOnce = true;\n\n\t\tif (wasRunning) this.start();\n\t}\n\n\tnextDate() {\n\t\treturn this.cronTime.sendAt();\n\t}\n\n\tasync fireOnTick() {\n\t\t// skip job if previous callback is still running\n\t\tif (this.waitForCompletion && this._isCallbackRunning) return;\n\n\t\tthis._isCallbackRunning = true;\n\n\t\t// handle errors in synchronous and asynchronous callbacks\n\t\ttry {\n\t\t\tfor (const callback of this._callbacks) {\n\t\t\t\tconst result = callback.call(\n\t\t\t\t\tthis.context,\n\t\t\t\t\tthis.onComplete as WithOnComplete<OC> extends true\n\t\t\t\t\t\t? CronOnCompleteCallback\n\t\t\t\t\t\t: never\n\t\t\t\t);\n\n\t\t\t\tif (\n\t\t\t\t\tresult &&\n\t\t\t\t\ttypeof result === 'object' &&\n\t\t\t\t\ttypeof result.then === 'function'\n\t\t\t\t) {\n\t\t\t\t\tif (this.waitForCompletion) {\n\t\t\t\t\t\tawait result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult.catch(error => {\n\t\t\t\t\t\t\tif (this.errorHandler != null) this.errorHandler(error);\n\t\t\t\t\t\t\telse console.error('[Cron] error in callback', error);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (this.errorHandler != null) this.errorHandler(error);\n\t\t\telse console.error('[Cron] error in callback', error);\n\t\t} finally {\n\t\t\tthis._isCallbackRunning = false;\n\t\t}\n\t}\n\n\tnextDates(i?: number) {\n\t\treturn this.cronTime.sendAt(i ?? 0);\n\t}\n\n\tstart() {\n\t\tif (this._isActive) return;\n\t\tthis._isActive = true;\n\n\t\tconst MAXDELAY = 2147483647; // the maximum number of milliseconds setTimeout will wait.\n\t\tlet timeout = this.cronTime.getTimeout();\n\t\tlet remaining = 0;\n\t\tlet startTime: number;\n\n\t\tconst setCronTimeout = (t: number) => {\n\t\t\tstartTime = Date.now();\n\t\t\t// using Math.max to avoid Node warnings with negative timeouts\n\t\t\t// see https://github.com/kelektiv/node-cron/issues/1000\n\t\t\tthis._timeout = setTimeout(callbackWrapper, Math.max(t, 1));\n\t\t\tif (this.unrefTimeout && typeof this._timeout.unref === 'function') {\n\t\t\t\tthis._timeout.unref();\n\t\t\t}\n\t\t};\n\n\t\t// the callback wrapper checks if it needs to sleep another period or not\n\t\t// and does the real callback logic when it's time.\n\t\tconst callbackWrapper = () => {\n\t\t\tconst diff = startTime + timeout - Date.now();\n\n\t\t\tif (diff > 0) {\n\t\t\t\tlet newTimeout = this.cronTime.getTimeout();\n\n\t\t\t\tif (newTimeout > diff) {\n\t\t\t\t\tnewTimeout = diff;\n\t\t\t\t}\n\n\t\t\t\tremaining += newTimeout;\n\t\t\t}\n\n\t\t\t// if there is sleep time remaining, calculate how long and go to sleep\n\t\t\t// again. This processing might make us miss the deadline by a few ms\n\t\t\t// times the number of sleep sessions. Given a MAXDELAY of almost a\n\t\t\t// month, this should be no issue.\n\t\t\tif (remaining) {\n\t\t\t\tif (remaining > MAXDELAY) {\n\t\t\t\t\tremaining -= MAXDELAY;\n\t\t\t\t\ttimeout = MAXDELAY;\n\t\t\t\t} else {\n\t\t\t\t\ttimeout = remaining;\n\t\t\t\t\tremaining = 0;\n\t\t\t\t}\n\n\t\t\t\tsetCronTimeout(timeout);\n\t\t\t} else {\n\t\t\t\t// we have arrived at the correct point in time.\n\t\t\t\tthis.lastExecution = new Date();\n\n\t\t\t\tthis._isActive = false;\n\n\t\t\t\t// start before calling back so the callbacks have the ability to stop the cron job\n\t\t\t\tif (!this.runOnce) this.start();\n\n\t\t\t\tvoid this.fireOnTick();\n\t\t\t}\n\t\t};\n\n\t\tif (timeout >= 0) {\n\t\t\t// don't try to sleep more than MAXDELAY ms at a time.\n\n\t\t\tif (timeout > MAXDELAY) {\n\t\t\t\tremaining = timeout - MAXDELAY;\n\t\t\t\ttimeout = MAXDELAY;\n\t\t\t}\n\n\t\t\tsetCronTimeout(timeout);\n\t\t} else {\n\t\t\t// handle negative timeout\n\t\t\tconst absoluteTimeout = Math.abs(timeout);\n\n\t\t\tconst message = `[Cron] Missed execution deadline by ${absoluteTimeout}ms for job${this.name ? ` \"${this.name}\"` : ''} with cron expression '${String(this.cronTime.source)}'`;\n\n\t\t\tif (absoluteTimeout <= this.threshold) {\n\t\t\t\t// execute immediately if within threshold\n\t\t\t\tconsole.warn(`${message}. Executing immediately.`);\n\n\t\t\t\tthis.lastExecution = new Date();\n\t\t\t\tvoid this.fireOnTick();\n\t\t\t} else {\n\t\t\t\t// skip job if beyond threshold\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`${message}. Skipping execution as it exceeds threshold (${this.threshold}ms).`\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttimeout = this.cronTime.getTimeout();\n\t\t\tsetCronTimeout(timeout);\n\t\t}\n\t}\n\n\tlastDate() {\n\t\treturn this.lastExecution;\n\t}\n\n\tprivate async _executeOnComplete() {\n\t\tif (typeof this.onComplete !== 'function') return;\n\n\t\ttry {\n\t\t\tawait this.onComplete.call(this.context);\n\t\t} catch (error) {\n\t\t\tconsole.error('[Cron] error in onComplete callback:', error);\n\t\t}\n\t}\n\n\tprivate async _waitForJobCompletion() {\n\t\twhile (this._isCallbackRunning) {\n\t\t\tawait new Promise(resolve => setTimeout(resolve, 100));\n\t\t}\n\t}\n\n\t/**\n\t * stop the cronjob.\n\t */\n\tstop() {\n\t\tif (this._timeout) clearTimeout(this._timeout);\n\t\tthis._isActive = false;\n\n\t\tif (!this.waitForCompletion) {\n\t\t\tvoid this._executeOnComplete();\n\t\t\treturn;\n\t\t}\n\n\t\treturn Promise.resolve().then(async () => {\n\t\t\tawait this._waitForJobCompletion();\n\t\t\tawait this._executeOnComplete();\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/time.ts",
    "content": "import { DateTime, Zone } from 'luxon';\n\nimport {\n\tALIASES,\n\tCONSTRAINTS,\n\tPARSE_DEFAULTS,\n\tPRESETS,\n\tRE_RANGE,\n\tRE_WILDCARDS,\n\tTIME_UNITS,\n\tTIME_UNITS_LEN,\n\tTIME_UNITS_MAP\n} from './constants';\nimport { CronError, ExclusiveParametersError } from './errors';\nimport {\n\tCronJobParams,\n\tRanges,\n\tTimeUnit,\n\tTimeUnitField\n} from './types/cron.types';\n\ntype CustomZone = Zone & {\n\tzoneName?: string;\n\tfixed?: string;\n};\n\ntype CustomDateTime = Omit<DateTime, 'zone'> & {\n\tzone: CustomZone;\n};\n\nexport class CronTime {\n\tsource: string | DateTime;\n\ttimeZone?: string;\n\tutcOffset?: number;\n\trealDate = false;\n\n\tprivate second: TimeUnitField<'second'> = {};\n\tprivate minute: TimeUnitField<'minute'> = {};\n\tprivate hour: TimeUnitField<'hour'> = {};\n\tprivate dayOfMonth: TimeUnitField<'dayOfMonth'> = {};\n\tprivate month: TimeUnitField<'month'> = {};\n\tprivate dayOfWeek: TimeUnitField<'dayOfWeek'> = {};\n\n\tconstructor(\n\t\tsource: CronJobParams['cronTime'],\n\t\ttimeZone?: CronJobParams['timeZone'],\n\t\tutcOffset?: null\n\t);\n\tconstructor(\n\t\tsource: CronJobParams['cronTime'],\n\t\ttimeZone?: null,\n\t\tutcOffset?: CronJobParams['utcOffset']\n\t);\n\tconstructor(\n\t\tsource: CronJobParams['cronTime'],\n\t\ttimeZone?: CronJobParams['timeZone'],\n\t\tutcOffset?: CronJobParams['utcOffset']\n\t) {\n\t\t// runtime check for JS users\n\t\tif (timeZone != null && utcOffset != null) {\n\t\t\tthrow new ExclusiveParametersError('timeZone', 'utcOffset');\n\t\t}\n\n\t\tif (timeZone) {\n\t\t\tconst dt = DateTime.fromObject({}, { zone: timeZone });\n\t\t\tif (!dt.isValid) {\n\t\t\t\tthrow new CronError('Invalid timezone.');\n\t\t\t}\n\n\t\t\tthis.timeZone = timeZone;\n\t\t}\n\n\t\tif (utcOffset != null) {\n\t\t\tthis.utcOffset = utcOffset;\n\t\t}\n\n\t\tif (timeZone == null && utcOffset == null) {\n\t\t\tconst systemTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\t\t\tthis.timeZone = systemTimezone;\n\t\t}\n\n\t\tif (source instanceof Date || source instanceof DateTime) {\n\t\t\tthis.source =\n\t\t\t\tsource instanceof Date ? DateTime.fromJSDate(source) : source;\n\t\t\tthis.realDate = true;\n\t\t} else {\n\t\t\tthis.source = source;\n\t\t\tthis._parse(this.source);\n\t\t}\n\t}\n\n\tstatic validateCronExpression(cronExpression: string): {\n\t\tvalid: boolean;\n\t\terror?: CronError;\n\t} {\n\t\ttry {\n\t\t\tnew CronTime(cronExpression);\n\t\t\treturn {\n\t\t\t\tvalid: true\n\t\t\t};\n\t\t} catch (error: any) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\terror\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate _getWeekDay(date: DateTime) {\n\t\treturn date.weekday === 7 ? 0 : date.weekday;\n\t}\n\n\t/**\n\t * calculate the \"next\" scheduled time\n\t */\n\tsendAt(): DateTime;\n\tsendAt(i: number): DateTime[];\n\tsendAt(i?: number): DateTime | DateTime[] {\n\t\tlet date =\n\t\t\tthis.realDate && this.source instanceof DateTime\n\t\t\t\t? this.source\n\t\t\t\t: DateTime.utc();\n\n\t\tif (this.timeZone) {\n\t\t\tdate = date.setZone(this.timeZone);\n\t\t}\n\n\t\tif (this.utcOffset !== undefined) {\n\t\t\tconst sign = this.utcOffset < 0 ? '-' : '+';\n\n\t\t\tconst offsetHours = Math.trunc(this.utcOffset / 60);\n\t\t\tconst offsetHoursStr = String(Math.abs(offsetHours)).padStart(2, '0');\n\n\t\t\tconst offsetMins = Math.abs(this.utcOffset - offsetHours * 60);\n\t\t\tconst offsetMinsStr = String(offsetMins).padStart(2, '0');\n\n\t\t\tconst utcZone = `UTC${sign}${offsetHoursStr}:${offsetMinsStr}`;\n\n\t\t\tdate = date.setZone(utcZone);\n\n\t\t\tif (!date.isValid) {\n\t\t\t\tthrow new CronError('ERROR: You specified an invalid UTC offset.');\n\t\t\t}\n\t\t}\n\n\t\tif (this.realDate) {\n\t\t\tif (DateTime.local() > date) {\n\t\t\t\tthrow new CronError('WARNING: Date in past. Will never be fired.');\n\t\t\t}\n\n\t\t\treturn date;\n\t\t}\n\n\t\tif (i === undefined || isNaN(i) || i < 0) {\n\t\t\tconst nextDate = this.getNextDateFrom(date);\n\t\t\t// just get the next scheduled time\n\t\t\treturn nextDate;\n\t\t} else {\n\t\t\t// return the next schedule times\n\t\t\tconst dates: DateTime[] = [];\n\t\t\tfor (; i > 0; i--) {\n\t\t\t\tdate = this.getNextDateFrom(date);\n\t\t\t\tdates.push(date);\n\t\t\t}\n\n\t\t\treturn dates;\n\t\t}\n\t}\n\n\t/**\n\t * get the number of milliseconds in the future at which to fire our callbacks.\n\t *\n\t * Can return a negative value when `sendAt` took too long to execute.\n\t * This is then handled in `CronJob` to execute the job immediately or skip\n\t * this execution based on the `threshold` option.\n\t *\n\t * We could instead call DateTime.local before `sendAt` to get the current time, but\n\t * then the calculated timeout would be offset by the time it takes to execute `sendAt`.\n\t *\n\t * As such it is better to handle negative timeouts by executing the job immediately.\n\t */\n\tgetTimeout() {\n\t\treturn this.sendAt().toMillis() - DateTime.local().toMillis();\n\t}\n\n\t/**\n\t * writes out a cron string\n\t */\n\ttoString() {\n\t\treturn this.toJSON().join(' ');\n\t}\n\n\t/**\n\t * json representation of the parsed cron syntax.\n\t */\n\ttoJSON() {\n\t\treturn TIME_UNITS.map(unit => {\n\t\t\treturn this._wcOrAll(unit);\n\t\t});\n\t}\n\n\t/**\n\t * get next date matching the specified cron time.\n\t *\n\t * Algorithm:\n\t * - Start with a start date and a parsed CronTime.\n\t * - Within the loop:\n\t *   - If we can't find an execution time within 8 years, throw an exception.\n\t *   - Find the next month to run at.\n\t *   - Find the next day of the month to run at.\n\t *   - Find the next day of the week to run at.\n\t *   - Find the next hour to run at.\n\t *   - Find the next minute to run at.\n\t *   - Find the next second to run at.\n\t *   - Check that the chosen time does not equal the current execution.\n\t * - Return the selected date object.\n\t */\n\tgetNextDateFrom(\n\t\tstart: Date | CustomDateTime,\n\t\ttimeZone?: string | CustomZone\n\t): DateTime {\n\t\tif (start instanceof Date) {\n\t\t\tstart = DateTime.fromJSDate(start);\n\t\t}\n\t\tif (timeZone) {\n\t\t\tstart = start.setZone(timeZone);\n\t\t} else {\n\t\t\ttimeZone = start.zone.zoneName ?? start.zone.fixed;\n\t\t}\n\t\t// make a clone in UTC so we can manipulate it as if there were no time zones\n\t\tlet date = DateTime.fromFormat(\n\t\t\t`${start.year}-${start.month}-${start.day} ${start.hour}:${start.minute}:${start.second}`,\n\t\t\t'yyyy-M-d H:m:s',\n\t\t\t{\n\t\t\t\tzone: 'UTC'\n\t\t\t}\n\t\t);\n\t\tconst firstDate = date.toMillis();\n\t\tif (!this.realDate) {\n\t\t\tif (date.millisecond > 0) {\n\t\t\t\tdate = date.set({ millisecond: 0, second: date.second + 1 });\n\t\t\t}\n\t\t}\n\n\t\tif (!date.isValid) {\n\t\t\tthrow new CronError('ERROR: You specified an invalid date.');\n\t\t}\n\n\t\t/**\n\t\t * maximum match interval is 8 years:\n\t\t * crontab has '* * 29 2 *' and we are on 1 March 2096:\n\t\t * next matching time will be 29 February 2104\n\t\t * source: https://github.com/cronie-crond/cronie/blob/0d669551680f733a4bdd6bab082a0b3d6d7f089c/src/cronnext.c#L401-L403\n\t\t */\n\t\tconst maxMatch = DateTime.now().plus({ years: 8 });\n\t\t// determine next date\n\t\t// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n\t\twhile (true) {\n\t\t\t// hard stop if the current date is after the maximum match interval\n\t\t\tif (date > maxMatch) {\n\t\t\t\tthrow new CronError(\n\t\t\t\t\t`Something went wrong. No execution date was found in the next 8 years.\n\t\t\t\t\t\t\tPlease provide the following string if you would like to help debug:\n\t\t\t\t\t\t\tTime Zone: ${\n\t\t\t\t\t\t\t\ttimeZone?.toString() ?? '\"\"'\n\t\t\t\t\t\t\t} - Cron String: ${this.source.toString()} - UTC offset: ${\n\t\t\t\t\t\t\t\tdate.offset\n\t\t\t\t\t\t\t} - current Date: ${DateTime.local().toString()}`\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!(date.month in this.month) &&\n\t\t\t\tObject.keys(this.month).length !== 12\n\t\t\t) {\n\t\t\t\tdate = date.plus({ month: 1 });\n\t\t\t\tdate = date.set({ day: 1, hour: 0, minute: 0, second: 0 });\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t(!(date.day in this.dayOfMonth) &&\n\t\t\t\t\tObject.keys(this.dayOfMonth).length !== 31 &&\n\t\t\t\t\t!(\n\t\t\t\t\t\tthis._getWeekDay(date) in this.dayOfWeek &&\n\t\t\t\t\t\tObject.keys(this.dayOfWeek).length !== 7\n\t\t\t\t\t)) ||\n\t\t\t\t(!(this._getWeekDay(date) in this.dayOfWeek) &&\n\t\t\t\t\tObject.keys(this.dayOfWeek).length !== 7 &&\n\t\t\t\t\t!(\n\t\t\t\t\t\tdate.day in this.dayOfMonth &&\n\t\t\t\t\t\tObject.keys(this.dayOfMonth).length !== 31\n\t\t\t\t\t))\n\t\t\t) {\n\t\t\t\tdate = date.plus({ days: 1 });\n\t\t\t\tdate = date.set({ hour: 0, minute: 0, second: 0 });\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!(date.hour in this.hour) && Object.keys(this.hour).length !== 24) {\n\t\t\t\t// only allow the hour to be 24 if a day hasn't passed yet since we started calculating the new time\n\t\t\t\t// otherwise we'll be changing the day here even though we already determined the correct day\n\n\t\t\t\tdate = date.plus({ hour: 1 });\n\t\t\t\tdate = date.set({ minute: 0, second: 0 });\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!(date.minute in this.minute) &&\n\t\t\t\tObject.keys(this.minute).length !== 60\n\t\t\t) {\n\t\t\t\tdate = date.plus({ minute: 1 });\n\t\t\t\tdate = date.set({ second: 0 });\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// respond to the previous date being checked by advancing a second\n\t\t\t// just like when we advance seconds normally\n\t\t\tif (\n\t\t\t\tdate.toMillis() === firstDate ||\n\t\t\t\t(!(date.second in this.second) &&\n\t\t\t\t\tObject.keys(this.second).length !== 60)\n\t\t\t) {\n\t\t\t\tdate = date.plus({ second: 1 });\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tbreak;\n\t\t}\n\n\t\t// handle cases where time jumps forward due to Daylight Savings\n\n\t\tconst expectedHour = date.hour;\n\t\tconst expectedMinute = date.minute;\n\n\t\tdate = DateTime.fromFormat(\n\t\t\t`${date.year}-${date.month}-${date.day} ${date.hour}:${date.minute}:${date.second}`,\n\t\t\t'yyyy-M-d H:m:s',\n\t\t\t{\n\t\t\t\tzone: timeZone\n\t\t\t}\n\t\t);\n\t\tconst nonDSTReferenceDate = DateTime.fromFormat(\n\t\t\t`${date.year}-1-1 0:0:0`,\n\t\t\t'yyyy-M-d H:m:s',\n\t\t\t{ zone: timeZone }\n\t\t);\n\n\t\t// if the hour or minute is different from expected and\n\t\t// if date we assume to not be under daylight savings has a different offset\n\t\t// rewind until just after the offset\n\t\tif (\n\t\t\t(expectedHour !== date.hour || expectedMinute !== date.minute) &&\n\t\t\tnonDSTReferenceDate.offset !== date.offset\n\t\t) {\n\t\t\twhile (date.minus({ minute: 1 }).offset !== nonDSTReferenceDate.offset) {\n\t\t\t\tdate = date.minus({ minute: 1 });\n\t\t\t}\n\t\t\treturn date;\n\t\t}\n\n\t\t// handle cases where time jumps back due to Daylight Savings (ambiguous times)\n\n\t\t// daylight savings jumps are either 60 or 30 minutes\n\t\tconst hourTestDate = date.minus({ hour: 1 });\n\t\tconst twoHourTestDate = date.minus({ hour: 2 });\n\t\t// if the previous hour is the same as this hour we are in an ambiguous time\n\t\t// jump back to the earlier hour as long as it's not in the past\n\t\tif (\n\t\t\t(hourTestDate.hour === date.hour ||\n\t\t\t\ttwoHourTestDate.hour === hourTestDate.hour) &&\n\t\t\thourTestDate > start\n\t\t) {\n\t\t\tdate = hourTestDate;\n\t\t}\n\t\t// similar for half hour jumps\n\t\tconst halfHourTestDate = date.minus({ minute: 30 });\n\t\tif (\n\t\t\t(halfHourTestDate.minute === date.minute ||\n\t\t\t\thourTestDate.minute === halfHourTestDate.minute) &&\n\t\t\thalfHourTestDate > start\n\t\t) {\n\t\t\tdate = halfHourTestDate;\n\t\t}\n\n\t\treturn date;\n\t}\n\n\t/**\n\t * wildcard, or all params in array (for to string)\n\t */\n\tprivate _wcOrAll(unit: TimeUnit) {\n\t\tif (this._hasAll(unit)) {\n\t\t\treturn '*';\n\t\t}\n\n\t\tconst all = [];\n\t\tfor (const time in this[unit]) {\n\t\t\tall.push(time);\n\t\t}\n\n\t\treturn all.join(',');\n\t}\n\n\tprivate _hasAll(unit: TimeUnit) {\n\t\tconst constraints = CONSTRAINTS[unit];\n\t\tconst low = constraints[0];\n\t\tconst high =\n\t\t\tunit === TIME_UNITS_MAP.DAY_OF_WEEK ? constraints[1] - 1 : constraints[1];\n\n\t\tfor (let i = low, n = high; i < n; i++) {\n\t\t\tif (!(i in this[unit])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * parse the cron syntax into something useful for selecting the next execution time.\n\t *\n\t * Algorithm:\n\t * - Replace preset\n\t * - Replace aliases in the source.\n\t * - Trim string and split for processing.\n\t * - Loop over split options (ms -> month):\n\t *   - Get the value (or default) in the current position.\n\t *   - Parse the value.\n\t */\n\tprivate _parse(source: string) {\n\t\tsource = source.toLowerCase();\n\n\t\tif (Object.keys(PRESETS).includes(source)) {\n\t\t\tsource = PRESETS[source as keyof typeof PRESETS];\n\t\t}\n\n\t\tsource = source.replace(/[a-z]{1,3}/gi, (alias: string) => {\n\t\t\tif (Object.keys(ALIASES).includes(alias)) {\n\t\t\t\treturn ALIASES[alias as keyof typeof ALIASES].toString();\n\t\t\t}\n\n\t\t\tthrow new CronError(`Unknown alias: ${alias}`);\n\t\t});\n\n\t\tconst units = source.trim().split(/\\s+/);\n\n\t\t// seconds are optional\n\t\tif (units.length < TIME_UNITS_LEN - 1) {\n\t\t\tthrow new CronError('Too few fields');\n\t\t}\n\n\t\tif (units.length > TIME_UNITS_LEN) {\n\t\t\tthrow new CronError('Too many fields');\n\t\t}\n\n\t\tconst unitsLen = units.length;\n\t\tfor (const unit of TIME_UNITS) {\n\t\t\tconst i = TIME_UNITS.indexOf(unit);\n\t\t\t// if the split source string doesn't contain all digits,\n\t\t\t// assume defaults for first n missing digits.\n\t\t\t// this adds support for 5-digit standard cron syntax\n\t\t\tconst cur =\n\t\t\t\tunits[i - (TIME_UNITS_LEN - unitsLen)] ?? PARSE_DEFAULTS[unit];\n\t\t\tthis._parseField(cur, unit);\n\t\t}\n\t}\n\n\t/**\n\t * parse individual field from the cron syntax provided.\n\t *\n\t * Algorithm:\n\t * - Split field by commas and check for wildcards to ensure proper user.\n\t * - Replace wildcard values with <low>-<high> boundaries.\n\t * - Split field by commas and then iterate over ranges inside field.\n\t *   - If range matches pattern then map over matches using replace (to parse the range by the regex pattern)\n\t *   - Starting with the lower bounds of the range iterate by step up to the upper bounds and toggle the CronTime field value flag on.\n\t */\n\n\tprivate _parseField(value: string, unit: TimeUnit) {\n\t\tconst typeObj = this[unit] as TimeUnitField<typeof unit>;\n\t\tlet pointer: Ranges[typeof unit];\n\n\t\tconst constraints = CONSTRAINTS[unit];\n\t\tconst low = constraints[0];\n\t\tconst high = constraints[1];\n\n\t\tconst fields = value.split(',');\n\t\tfields.forEach(field => {\n\t\t\tconst wildcardIndex = field.indexOf('*');\n\t\t\tif (wildcardIndex !== -1 && wildcardIndex !== 0) {\n\t\t\t\tthrow new CronError(\n\t\t\t\t\t`Field (${field}) has an invalid wildcard expression`\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\t// \"*\" is a shortcut to [low-high] range for the field\n\t\tvalue = value.replace(RE_WILDCARDS, `${low}-${high}`);\n\n\t\t// commas separate information, so split based on those\n\t\tconst allRanges = value.split(',');\n\n\t\tfor (const range of allRanges) {\n\t\t\tconst match = [...range.matchAll(RE_RANGE)][0];\n\t\t\tif (match?.[1] !== undefined) {\n\t\t\t\tconst [, mLower, mUpper, mStep] = match;\n\t\t\t\tlet lower = parseInt(mLower, 10);\n\t\t\t\tlet upper = mUpper !== undefined ? parseInt(mUpper, 10) : undefined;\n\n\t\t\t\tconst wasStepDefined = mStep !== undefined;\n\t\t\t\tconst step = parseInt(mStep ?? '1', 10);\n\t\t\t\tif (step === 0) {\n\t\t\t\t\tthrow new CronError(`Field (${unit}) has a step of zero`);\n\t\t\t\t}\n\n\t\t\t\tif (upper !== undefined && lower > upper) {\n\t\t\t\t\tthrow new CronError(`Field (${unit}) has an invalid range`);\n\t\t\t\t}\n\n\t\t\t\tconst isOutOfRange =\n\t\t\t\t\tlower < low ||\n\t\t\t\t\t(upper !== undefined && upper > high) ||\n\t\t\t\t\t(upper === undefined && lower > high);\n\n\t\t\t\tif (isOutOfRange) {\n\t\t\t\t\tthrow new CronError(`Field value (${value}) is out of range`);\n\t\t\t\t}\n\n\t\t\t\t// positive integer higher than constraints[0]\n\t\t\t\tlower = Math.min(Math.max(low, ~~Math.abs(lower)), high);\n\n\t\t\t\t// positive integer lower than constraints[1]\n\t\t\t\tif (upper !== undefined) {\n\t\t\t\t\tupper = Math.min(high, ~~Math.abs(upper));\n\t\t\t\t} else {\n\t\t\t\t\t// if step is provided, the default upper range is the highest value\n\t\t\t\t\tupper = wasStepDefined ? high : lower;\n\t\t\t\t}\n\n\t\t\t\t// count from the lower barrier to the upper\n\t\t\t\t// forcing type cast here since we checked above that\n\t\t\t\t// we are between constraint bounds\n\t\t\t\tpointer = lower as typeof pointer;\n\n\t\t\t\tdo {\n\t\t\t\t\ttypeObj[pointer] = true; // mutates the field objects values inside CronTime\n\t\t\t\t\tpointer += step;\n\t\t\t\t} while (pointer <= upper);\n\n\t\t\t\t// merge day 7 into day 0 (both Sunday), and remove day 7\n\t\t\t\t// since we work with day-of-week 0-6 under the hood\n\t\t\t\tif (unit === 'dayOfWeek') {\n\t\t\t\t\tif (!typeObj[0] && !!typeObj[7]) typeObj[0] = typeObj[7];\n\t\t\t\t\tdelete typeObj[7];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthrow new CronError(`Field (${unit}) cannot be parsed`);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/types/cron.types.ts",
    "content": "import { SpawnOptions } from 'child_process';\nimport { DateTime } from 'luxon';\nimport { CONSTRAINTS, TIME_UNITS_MAP } from '../constants';\nimport { CronJob } from '../job';\nimport { IntRange } from './utils';\n\ninterface BaseCronJobParams<\n\tOC extends CronOnCompleteCommand | null = null,\n\tC = null\n> {\n\tcronTime: string | Date | DateTime;\n\tonTick: CronCommand<C, WithOnComplete<OC>>;\n\tonComplete?: OC;\n\tstart?: boolean | null;\n\tcontext?: C;\n\trunOnInit?: boolean | null;\n\tunrefTimeout?: boolean | null;\n\twaitForCompletion?: boolean | null;\n\terrorHandler?: ((error: unknown) => void) | null;\n\tthreshold?: number | null;\n\tname?: string | null;\n}\n\nexport type CronJobParams<\n\tOC extends CronOnCompleteCommand | null = null,\n\tC = null\n> = BaseCronJobParams<OC, C> &\n\t(\n\t\t| {\n\t\t\t\ttimeZone?: string | null;\n\t\t\t\tutcOffset?: never;\n\t\t  }\n\t\t| {\n\t\t\t\ttimeZone?: never;\n\t\t\t\tutcOffset?: number | null;\n\t\t  }\n\t);\n\nexport type CronContext<C> = C extends null ? CronJob : NonNullable<C>;\n\nexport type CronCallback<C, WithOnCompleteBool extends boolean = false> = (\n\tthis: CronContext<C>,\n\tonComplete: WithOnCompleteBool extends true ? CronOnCompleteCallback : never\n) => void | Promise<void>;\n\nexport type CronOnCompleteCallback = () => void | Promise<void>;\n\nexport type CronSystemCommand =\n\t| string\n\t| {\n\t\t\tcommand: string;\n\t\t\targs?: readonly string[] | null;\n\t\t\toptions?: SpawnOptions | null;\n\t  };\n\nexport type CronCommand<C, WithOnCompleteBool extends boolean = false> =\n\t| CronCallback<C, WithOnCompleteBool>\n\t| CronSystemCommand;\n\nexport type CronOnCompleteCommand = CronOnCompleteCallback | CronSystemCommand;\n\nexport type WithOnComplete<OC> = OC extends null ? false : true;\n\nexport type TimeUnit = (typeof TIME_UNITS_MAP)[keyof typeof TIME_UNITS_MAP];\n\nexport type TimeUnitField<T extends TimeUnit> = Partial<\n\tRecord<Ranges[T], boolean>\n>;\n\nexport interface Ranges {\n\tsecond: SecondRange;\n\tminute: MinuteRange;\n\thour: HourRange;\n\tdayOfMonth: DayOfMonthRange;\n\tmonth: MonthRange;\n\tdayOfWeek: DayOfWeekRange;\n}\n\nexport type SecondRange = IntRange<\n\t(typeof CONSTRAINTS)['second'][0],\n\t(typeof CONSTRAINTS)['second'][1]\n>;\nexport type MinuteRange = IntRange<\n\t(typeof CONSTRAINTS)['minute'][0],\n\t(typeof CONSTRAINTS)['minute'][1]\n>;\nexport type HourRange = IntRange<\n\t(typeof CONSTRAINTS)['hour'][0],\n\t(typeof CONSTRAINTS)['hour'][1]\n>;\nexport type DayOfMonthRange = IntRange<\n\t(typeof CONSTRAINTS)['dayOfMonth'][0],\n\t(typeof CONSTRAINTS)['dayOfMonth'][1]\n>;\nexport type MonthRange = IntRange<\n\t(typeof CONSTRAINTS)['month'][0],\n\t(typeof CONSTRAINTS)['month'][1]\n>;\nexport type DayOfWeekRange = IntRange<\n\t(typeof CONSTRAINTS)['dayOfWeek'][0],\n\t(typeof CONSTRAINTS)['dayOfWeek'][1]\n>;\n"
  },
  {
    "path": "src/types/utils.ts",
    "content": "export type IntRange<F extends number, T extends number> = Exclude<\n\tEnumerate<T>,\n\tEnumerate<F, false>\n>;\n\ntype Enumerate<\n\tN extends number,\n\tWithTail extends boolean = true,\n\tAcc extends number[] = []\n> = Acc['length'] extends N\n\t? WithTail extends true\n\t\t? [...Acc, Acc['length']][number]\n\t\t: Acc[number]\n\t: Enumerate<N, WithTail, [...Acc, Acc['length']]>;\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import { Ranges } from './types/cron.types';\n\nexport const getRecordKeys = <K extends Ranges[keyof Ranges]>(\n\trecord: Partial<Record<K, boolean>>\n) => {\n\treturn Object.keys(record) as unknown as (keyof typeof record)[];\n};\n"
  },
  {
    "path": "tests/cron.fuzz.ts",
    "content": "/* eslint-disable jest/no-standalone-expect */\nimport { fc, test } from '@fast-check/jest';\nimport { CronJob } from '../src/job';\nimport { CronError } from '../src/errors';\n\n/**\n * fuzzing might result in an infinite loop in our code, so Jest will simply timeout.\n * experimental worker implementation of ```@fast-check/jest``` could help detect that issue\n * that would be better as it would also give the counter-example that causes the bug\n * but since it is still experimental, simply uncomment the log line in testCronJob\n * function, so you can see the input causing the infinite loop.\n */\nfunction testCronJob(\n\t{\n\t\tcronTime,\n\t\tstart,\n\t\ttimeZone,\n\t\trunOnInit,\n\t\tutcOffset,\n\t\tunrefTimeout,\n\t\ttzOrOffset\n\t}: {\n\t\tcronTime: string;\n\t\tstart: boolean;\n\t\ttimeZone: string;\n\t\trunOnInit: boolean;\n\t\tutcOffset: number;\n\t\tunrefTimeout: boolean;\n\t\ttzOrOffset: boolean;\n\t},\n\tcheckError: (err: unknown) => boolean\n) {\n\t// console.debug(\n\t// \t`${cronTime} | ${start} | ${timeZone} | ${runOnInit} | ${utcOffset} | ${unrefTimeout} | ${tzOrOffset}`\n\t// );\n\ttry {\n\t\tconst job = new CronJob(\n\t\t\tcronTime,\n\t\t\tfunction () {},\n\t\t\tnull,\n\t\t\tstart,\n\t\t\t(tzOrOffset ? timeZone : null) as typeof tzOrOffset extends true\n\t\t\t\t? string\n\t\t\t\t: null,\n\t\t\tnull,\n\t\t\trunOnInit,\n\t\t\t(tzOrOffset ? null : utcOffset) as typeof tzOrOffset extends true\n\t\t\t\t? null\n\t\t\t\t: number,\n\t\t\tunrefTimeout\n\t\t);\n\n\t\texpect(job.isActive).toBe(start);\n\t\tjob.stop();\n\t\texpect(job.isActive).toBe(false);\n\n\t\texpect(job.cronTime.source).toBe(cronTime);\n\t} catch (error) {\n\t\tconst isOk = checkError(error);\n\t\tif (!isOk) {\n\t\t\tconsole.error(error);\n\t\t\tconsole.error(\n\t\t\t\t'Make sure the relevant code is using an instance of CronError (or derived) when throwing.'\n\t\t\t);\n\t\t}\n\t\texpect(isOk).toBe(true);\n\t}\n}\n\ntest.prop(\n\t{\n\t\tcronTime: fc.stringMatching(/^(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ){5,6}$/),\n\t\tstart: fc.boolean(),\n\t\ttimeZone: fc.stringMatching(\n\t\t\t/^((?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])|(Africa\\/Abidjan|Asia\\/Singapore|Australia\\/Sydney|CET|EST|Europe\\/Paris|America\\/New_York))$/\n\t\t),\n\t\trunOnInit: fc.boolean(),\n\t\tutcOffset: fc.integer(),\n\t\tunrefTimeout: fc.boolean(),\n\t\ttzOrOffset: fc.boolean()\n\t},\n\t{ numRuns: 100_000 }\n)(\n\t'CronJob should behave as expected and not error unexpectedly (with matching inputs)',\n\tparams => {\n\t\ttestCronJob(params, err => err instanceof CronError);\n\t}\n);\n\ntest.prop(\n\t{\n\t\tcronTime: fc.string(),\n\t\tstart: fc.boolean(),\n\t\ttimeZone: fc.string(),\n\t\trunOnInit: fc.boolean(),\n\t\tutcOffset: fc.integer(),\n\t\tunrefTimeout: fc.boolean(),\n\t\ttzOrOffset: fc.boolean()\n\t},\n\t{ numRuns: 100_000 }\n)(\n\t'CronJob should behave as expected and not error unexpectedly (with random inputs)',\n\tparams => {\n\t\ttestCronJob(params, err => err instanceof CronError);\n\t}\n);\n\ntest.prop(\n\t{\n\t\tcronTime: fc.anything(),\n\t\tstart: fc.anything(),\n\t\ttimeZone: fc.anything(),\n\t\trunOnInit: fc.anything(),\n\t\tutcOffset: fc.anything(),\n\t\tunrefTimeout: fc.anything(),\n\t\ttzOrOffset: fc.boolean()\n\t},\n\t{ numRuns: 100_000 }\n)(\n\t'CronJob should behave as expected and not error unexpectedly (with anything inputs)',\n\tparams => {\n\t\ttestCronJob(\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\n\t\t\tparams as any,\n\t\t\terr => err instanceof CronError || err instanceof TypeError\n\t\t);\n\t}\n);\n"
  },
  {
    "path": "tests/cron.test.ts",
    "content": "import { DateTime } from 'luxon';\nimport sinon from 'sinon';\nimport { CronJob, CronTime } from '../src';\n\ndescribe('cron', () => {\n\tlet callback: jest.Mock;\n\n\tbeforeEach(() => {\n\t\tcallback = jest.fn();\n\t});\n\n\tafterAll(() => {\n\t\tjest.clearAllMocks();\n\t});\n\n\tafterEach(() => {\n\t\texpect.hasAssertions();\n\t\tsinon.restore();\n\t});\n\n\tit('should not stop job if sendAt takes time to complete (#962)', () => {\n\t\tconst EVERY = 5;\n\t\tconst TICK = EVERY * 1000;\n\t\tconst DELAY = 350;\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: `*/${EVERY} * * * * *`,\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 350\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(TICK)\n\t\t\t.onCall(1)\n\t\t\t.returns(-DELAY);\n\n\t\tconst clock = sinon.useFakeTimers();\n\n\t\t// mock console.warn to avoid polluting tests with the warning\n\t\tconst warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});\n\n\t\tjob.start();\n\n\t\tclock.tick(TICK);\n\t\texpect(job.isActive).toBe(true);\n\n\t\tjob.stop();\n\t\twarnSpy.mockRestore();\n\t});\n\n\tdescribe('with seconds', () => {\n\t\tit('should run every second (* * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('* * * * * *', callback, null, true);\n\n\t\t\texpect(callback).not.toHaveBeenCalled();\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run second with onComplete (* * * * * *)', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should use standard cron no-seconds syntax (* * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('* * * * *', callback, null, true);\n\n\t\t\tclock.tick(1000); // tick second\n\n\t\t\tclock.tick(59 * 1000); // tick minute\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run every second for 5 seconds (* * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('* * * * * *', callback, null, true);\n\t\t\tfor (let i = 0; i < 5; i++) {\n\t\t\t\tclock.tick(1000);\n\t\t\t}\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(5);\n\t\t});\n\n\t\tit('should run every second for 5 seconds with onComplete (* * * * * *)', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(5);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\tfor (let i = 0; i < 5; i++) clock.tick(1000);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should run every second for 5 seconds (*/1 * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('*/1 * * * * *', callback, null, true);\n\t\t\tfor (let i = 0; i < 5; i++) clock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(5);\n\t\t});\n\n\t\tit('should run every 2 seconds for 1 seconds (*/2 * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('*/2 * * * * *', callback, null, true);\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(0);\n\t\t});\n\n\t\tit('should run every 2 seconds for 5 seconds (*/2 * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('*/2 * * * * *', callback, null, true);\n\t\t\tfor (let i = 0; i < 5; i++) clock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\t});\n\n\t\tit('should run every second for 5 seconds with onComplete (*/1 * * * * *)', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob(\n\t\t\t\t'*/1 * * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(5);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tfor (let i = 0; i < 5; i++) clock.tick(1000);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should run every second for a range ([start]-[end] * * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('0-8 * * * * *', callback, null, true);\n\t\t\tclock.tick(10000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(8);\n\t\t});\n\n\t\tit('should run every second for a range ([start]-[end] * * * * *) with onComplete', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob(\n\t\t\t\t'0-8 * * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(8);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tclock.tick(10000);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should default to full range when upper range not provided (1/2 * * * * *)', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob(\n\t\t\t\t'1/2 * * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(30);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tclock.tick(1000 * 60);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should run every second (* * * * * *) using the object constructor', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: callback,\n\t\t\t\tstart: true\n\t\t\t});\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run every second with onComplete (* * * * * *) using the object constructor', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: callback,\n\t\t\t\tonComplete: () => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\tstart: true\n\t\t\t});\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t});\n\t});\n\n\tdescribe('with minutes', () => {\n\t\tit('should fire every 60 min', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst m60 = 60 * 60 * 1000;\n\t\t\tconst l: number[] = [];\n\t\t\tconst job = new CronJob(\n\t\t\t\t'00 30 * * * *',\n\t\t\t\t() => {\n\t\t\t\t\tl.push(Math.floor(Date.now() / 60000));\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\tclock.tick(m60 * 10);\n\n\t\t\texpect(l).toHaveLength(10);\n\t\t\texpect(l.every(i => i % 30 === 0)).toBe(true);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should run every 45 minutes for 2 hours (0 */45 * * * *)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob('0 */45 * * * *', callback, null, true);\n\t\t\tfor (let i = 0; i < 2; i++) clock.tick(60 * 60 * 1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(4);\n\t\t});\n\n\t\tit('should run every 45 minutes for 2 hours (0 */45 * * * *) with onComplete', done => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst job = new CronJob(\n\t\t\t\t'0 */45 * * * *',\n\t\t\t\tcallback,\n\t\t\t\t() => {\n\t\t\t\t\texpect(callback).toHaveBeenCalledTimes(4);\n\t\t\t\t\tdone();\n\t\t\t\t},\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tfor (let i = 0; i < 2; i++) clock.tick(60 * 60 * 1000);\n\t\t\tjob.stop();\n\t\t});\n\t});\n\n\tit('should start and stop job from outside', done => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst job = new CronJob(\n\t\t\t'* * * * * *',\n\t\t\tfunction () {\n\t\t\t\tcallback();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t\t\tdone();\n\t\t\t},\n\t\t\ttrue\n\t\t);\n\t\tclock.tick(1000);\n\t\tjob.stop();\n\t});\n\n\tit('should start and stop job from inside (default context)', done => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tnew CronJob(\n\t\t\t'* * * * * *',\n\t\t\tfunction () {\n\t\t\t\tcallback();\n\t\t\t\tthis.stop();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t\t\tdone();\n\t\t\t},\n\t\t\ttrue\n\t\t);\n\t\tclock.tick(1000);\n\t});\n\n\tdescribe('with date', () => {\n\t\tit('should run on a specific date', () => {\n\t\t\tconst d = new Date();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\t\t\tconst s = d.getSeconds() + 1;\n\t\t\td.setSeconds(s);\n\t\t\tconst job = new CronJob(\n\t\t\t\td,\n\t\t\t\t() => {\n\t\t\t\t\tconst t = new Date();\n\t\t\t\t\texpect(t.getSeconds()).toBe(d.getSeconds());\n\t\t\t\t\tcallback();\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run on a specific date and call onComplete from onTick', async () => {\n\t\t\tconst d = new Date();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\t\t\td.setSeconds(d.getSeconds() + 1);\n\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tconst job = new CronJob(\n\t\t\t\t\td,\n\t\t\t\t\tonComplete => {\n\t\t\t\t\t\tconst t = new Date();\n\t\t\t\t\t\texpect(t.getSeconds()).toBe(d.getSeconds());\n\t\t\t\t\t\tvoid onComplete();\n\t\t\t\t\t},\n\t\t\t\t\t() => {\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t},\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tclock.tick(1000);\n\t\t\t\tjob.stop();\n\t\t\t});\n\n\t\t\t// onComplete is called 2 times: once in onTick() & once when calling job.stop()\n\t\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\t});\n\n\t\tit('should run on a specific date and call onComplete from onTick using the object constructor', async () => {\n\t\t\tconst d = new Date();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\t\t\td.setSeconds(d.getSeconds() + 1);\n\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tconst job = CronJob.from({\n\t\t\t\t\tcronTime: d,\n\t\t\t\t\tonTick: onComplete => {\n\t\t\t\t\t\tconst t = new Date();\n\t\t\t\t\t\texpect(t.getSeconds()).toBe(d.getSeconds());\n\t\t\t\t\t\tvoid onComplete();\n\t\t\t\t\t},\n\t\t\t\t\tonComplete: function () {\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t} as () => void,\n\t\t\t\t\tstart: true\n\t\t\t\t});\n\t\t\t\tclock.tick(1000);\n\t\t\t\tjob.stop();\n\t\t\t});\n\n\t\t\t// onComplete is called 2 times: once in onTick() & once when calling job.stop()\n\t\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\t});\n\n\t\tit(\"should not be able to call onComplete from onTick if if wasn't provided\", () => {\n\t\t\texpect.assertions(4);\n\t\t\tconst d = new Date();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\t\t\td.setSeconds(d.getSeconds() + 1);\n\n\t\t\tconst job = new CronJob(\n\t\t\t\td,\n\t\t\t\tonComplete => {\n\t\t\t\t\tconst t = new Date();\n\t\t\t\t\texpect(onComplete).toBeUndefined();\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// @ts-expect-error should be a TS warning and throw\n\t\t\t\t\t\tonComplete();\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t// we make sure this isn't skipped with `expect.assertions()`\n\t\t\t\t\t\t// at the beginning of the test\n\t\t\t\t\t\t// eslint-disable-next-line jest/no-conditional-expect\n\t\t\t\t\t\texpect(e).toBeInstanceOf(TypeError);\n\t\t\t\t\t}\n\t\t\t\t\texpect(onComplete).toBeUndefined();\n\t\t\t\t\texpect(t.getSeconds()).toBe(d.getSeconds());\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should wait and not fire immediately', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst d = new Date().getTime() + 31 * 86400 * 1000;\n\n\t\t\tconst job = new CronJob(new Date(d), callback);\n\t\t\tjob.start();\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(0);\n\t\t});\n\n\t\tit('should wait but fire on init', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst d = new Date().getTime() + 31 * 86400 * 1000;\n\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: new Date(d),\n\t\t\t\tonTick: callback,\n\t\t\t\trunOnInit: true\n\t\t\t});\n\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t\tjob.start();\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should fire on init but not run until started', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: callback,\n\t\t\t\trunOnInit: true\n\t\t\t});\n\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t\tjob.start();\n\n\t\t\tclock.tick(3500);\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(4);\n\t\t});\n\t});\n\n\tdescribe('with timezone', () => {\n\t\tit('should run a job using cron syntax with a timezone', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tlet zone = 'America/Chicago';\n\t\t\t// new Orleans time\n\t\t\tlet t = DateTime.local().setZone(zone);\n\t\t\t// current time\n\t\t\tconst d = DateTime.local();\n\n\t\t\t// if current time is New Orleans time, switch to Los Angeles..\n\t\t\tif (t.hour === d.hour) {\n\t\t\t\tzone = 'America/Los_Angeles';\n\t\t\t\tt = t.setZone(zone);\n\t\t\t}\n\t\t\texpect(d.hour).not.toBe(t.hour);\n\n\t\t\t// if t = 59s12m then t.setSeconds(60)\n\t\t\t// becomes 00s13m so we're fine just doing\n\t\t\t// this and no testRun callback.\n\t\t\tt = t.plus({ seconds: 1 });\n\t\t\t// run a job designed to be executed at a given\n\t\t\t// time in `zone`, making sure that it is a different\n\t\t\t// hour than local time.\n\t\t\tconst job = new CronJob(\n\t\t\t\t`${t.second} ${t.minute} ${t.hour} * * *`,\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tzone\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run a job using cron syntax with a \"UTC+HH:mm\" offset as timezone', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\t// current time\n\t\t\tconst d = DateTime.local();\n\n\t\t\t// current time with zone offset\n\t\t\tlet zone = 'UTC+5:30';\n\t\t\tlet t = DateTime.local().setZone(zone);\n\n\t\t\t// if current offset is UTC+5:30, switch to UTC+6:30..\n\t\t\tif (t.hour === d.hour && t.minute === d.minute) {\n\t\t\t\tzone = 'UTC+6:30';\n\t\t\t\tt = t.setZone(zone);\n\t\t\t}\n\t\t\texpect(`${d.hour}:${d.minute}`).not.toBe(`${t.hour}:${t.minute}`);\n\n\t\t\t// if t = 59s12m then t.setSeconds(60)\n\t\t\t// becomes 00s13m so we're fine just doing\n\t\t\t// this and no testRun callback.\n\t\t\tt = t.plus({ seconds: 1 });\n\t\t\t// run a job designed to be executed at a given\n\t\t\t// time in `zone`, making sure that it is a different\n\t\t\t// hour than local time.\n\t\t\tconst job = new CronJob(\n\t\t\t\t`${t.second} ${t.minute} ${t.hour} * * *`,\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tzone\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run a job using a date', () => {\n\t\t\tlet zone = 'America/Chicago';\n\t\t\t// new Orleans time\n\t\t\tlet t = DateTime.local().setZone(zone);\n\t\t\t// current time\n\t\t\tlet d = DateTime.local();\n\n\t\t\t// if current time is New Orleans time, switch to Los Angeles..\n\t\t\tif (t.hour === d.hour) {\n\t\t\t\tzone = 'America/Los_Angeles';\n\t\t\t\tt = t.setZone(zone);\n\t\t\t}\n\n\t\t\texpect(d.hour).not.toBe(t.hour);\n\t\t\td = d.plus({ seconds: 1 });\n\t\t\tconst clock = sinon.useFakeTimers(d.valueOf());\n\t\t\tconst job = new CronJob(d.toJSDate(), callback, null, true, zone);\n\t\t\tclock.tick(1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\t// this test requires setting the TZ env variable\n\t\t// to Europe/Paris to run correctly on CI\n\t\tit('should use system timezone by default (issue #971)', () => {\n\t\t\tconst d = DateTime.fromISO('2025-03-28T00:00:00', {\n\t\t\t\tzone: 'Europe/Paris'\n\t\t\t}).toJSDate();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '1 0 * * *',\n\t\t\t\tonTick: callback,\n\t\t\t\tstart: true\n\t\t\t});\n\t\t\tclock.tick(60 * 60 * 1000);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should test if timezone is valid.', () => {\n\t\t\texpect(() => {\n\t\t\t\tCronJob.from({\n\t\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\t\tonTick: () => {},\n\t\t\t\t\ttimeZone: 'fake/timezone'\n\t\t\t\t});\n\t\t\t}).toThrow();\n\t\t});\n\t});\n\n\tdescribe('onTick scoping', () => {\n\t\tit('should scope onTick to running job', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tfunction () {\n\t\t\t\t\texpect(job).toBeInstanceOf(CronJob);\n\t\t\t\t\texpect(job).toEqual(this);\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should scope onTick to running job using the object constructor', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: function () {\n\t\t\t\t\texpect(job).toBeInstanceOf(CronJob);\n\t\t\t\t\texpect(job).toEqual(this);\n\t\t\t\t},\n\t\t\t\tstart: true\n\t\t\t});\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should scope onTick to object', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tfunction () {\n\t\t\t\t\texpect(this.hello).toBe('world');\n\t\t\t\t\texpect(job).not.toEqual(this);\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\t{ hello: 'world' }\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should scope onTick to object using the object constructor', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = CronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: function () {\n\t\t\t\t\texpect(this.hello).toBe('world');\n\t\t\t\t\texpect(job).not.toEqual(this);\n\t\t\t\t},\n\t\t\t\tstart: true,\n\t\t\t\tcontext: { hello: 'world' }\n\t\t\t});\n\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t});\n\t});\n\n\tit('should not get into an infinite loop on invalid times', () => {\n\t\texpect(() => {\n\t\t\tnew CronJob('* 60 * * * *', () => {}, null, true);\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronJob('* * 24 * * *', () => {}, null, true);\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronJob('0 0 30 FEB *', callback, null, true);\n\t\t}).toThrow();\n\t});\n\n\tit('should not get into an infinite loop when using uncommon offsets', () => {\n\t\tconst job = new CronJob(\n\t\t\t'* * * * * *',\n\t\t\tfunction () {},\n\t\t\tnull,\n\t\t\ttrue,\n\t\t\tnull,\n\t\t\tnull,\n\t\t\ttrue,\n\t\t\t-4\n\t\t);\n\t\texpect(job.isActive).toBe(true);\n\t\tjob.stop();\n\t});\n\n\tit('should not throw if at least one time is valid', () => {\n\t\texpect(() => {\n\t\t\tconst job = new CronJob('0 0 30 JAN,FEB *', callback, null, true);\n\t\t\tjob.stop();\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test start of month', () => {\n\t\tconst d = DateTime.fromISO('2024-12-31T23:59:59', {\n\t\t\tzone: 'Europe/Paris'\n\t\t}).toJSDate();\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = new CronJob('0 0 0 1 * *', callback, null, true);\n\n\t\t// first millisecond of January\n\t\tclock.tick(1001);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t// 2 ms less than 28 days; last millisecond of February\n\t\tclock.tick(28 * 24 * 60 * 60 * 1000 - 2);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t// 1 ms more than 31 days; jump over 2 firsts\n\t\tclock.tick(31 * 24 * 60 * 60 * 1000 + 1);\n\t\tjob.stop();\n\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t});\n\n\tit('should not fire if time was adjusted back', () => {\n\t\tconst clock = sinon.useFakeTimers({\n\t\t\ttoFake: ['setTimeout']\n\t\t});\n\n\t\tconst job = new CronJob('0 * * * * *', callback, null, true);\n\n\t\tclock.tick(60000);\n\t\texpect(callback).toHaveBeenCalledTimes(0);\n\n\t\tjob.stop();\n\t});\n\n\tit('should run every day at 3:59:59', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(59);\n\t\td.setMinutes(59);\n\t\td.setHours(23);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '59 59 3 * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true,\n\t\t\ttimeZone: 'America/Los_Angeles'\n\t\t});\n\n\t\tconst twoWeeks = 14 * 24 * 60 * 60 * 1000;\n\t\tclock.tick(twoWeeks);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(14);\n\t});\n\n\tit('should run every 2 hours between hours', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(0);\n\t\td.setMinutes(0);\n\t\td.setHours(0);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '0 2-6/2 * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true\n\t\t});\n\n\t\tclock.tick(2 * 60 * 1000);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tclock.tick(2 * 60 * 1000);\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\tclock.tick(2 * 60 * 1000);\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t\tclock.tick(2 * 60 * 1000);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t});\n\n\tit('should run every minute', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(0);\n\t\td.setMinutes(0);\n\t\td.setHours(0);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '00 * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true\n\t\t});\n\n\t\tclock.tick(60 * 1000);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tclock.tick(60 * 1000);\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t});\n\n\tit('should run every day at 12:30', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(0);\n\t\td.setMinutes(0);\n\t\td.setHours(0);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '00 30 00 * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true\n\t\t});\n\n\t\tconst day = 24 * 60 * 60 * 1000;\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t\tclock.tick(5 * day);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(8);\n\t});\n\n\tit('should trigger onTick at midnight', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(59);\n\t\td.setMinutes(59);\n\t\td.setHours(23);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '00 * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true,\n\t\t\ttimeZone: 'UTC'\n\t\t});\n\n\t\tclock.tick(1000); // move clock 1 second\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t});\n\n\tit('should run every day UTC', () => {\n\t\tconst d = new Date('12/31/2014');\n\t\td.setSeconds(0);\n\t\td.setMinutes(0);\n\t\td.setHours(0);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '00 30 00 * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true,\n\t\t\ttimeZone: 'UTC'\n\t\t});\n\n\t\tconst day = 24 * 60 * 60 * 1000;\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\tclock.tick(day);\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t\tclock.tick(5 * day);\n\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(8);\n\t});\n\n\t// from https://github.com/kelektiv/node-cron/issues/180#issuecomment-154108131\n\tit('should run once not double', () => {\n\t\tconst d = new Date(2015, 1, 1, 1, 1, 41, 0);\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: true\n\t\t});\n\n\t\tconst minute = 60 * 1000;\n\t\tclock.tick(minute);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t});\n\n\t/**\n\t * maximum match interval is 8 years:\n\t * crontab has '* * 29 2 *' and we are on 1 March 2096:\n\t * next matching time will be 29 February 2104\n\t * source: https://github.com/cronie-crond/cronie/blob/0d669551680f733a4bdd6bab082a0b3d6d7f089c/src/cronnext.c#L401-L403\n\t */\n\tit('should work correctly for max match interval', () => {\n\t\tconst d = DateTime.fromISO('2096-03-01T00:00:00', {\n\t\t\tzone: 'Europe/Paris'\n\t\t}).toJSDate();\n\n\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: ' * * 29 2 *',\n\t\t\tonTick: callback,\n\t\t\tstart: true\n\t\t});\n\n\t\t// 7 years, 11 months and 27 days\n\t\tconst almostEightYears = 2919 * 24 * 60 * 60 * 1000;\n\t\tclock.tick(almostEightYears);\n\t\texpect(callback).toHaveBeenCalledTimes(0);\n\n\t\t// tick by 1 day\n\t\tclock.tick(24 * 60 * 60 * 1000);\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t});\n\n\tdescribe('with utcOffset', () => {\n\t\tit('should run a job using cron syntax with number format utcOffset', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\t// current time\n\t\t\tconst t = DateTime.local();\n\t\t\t// uTC Offset decreased by an hour\n\t\t\tconst utcOffset = t.offset - 60;\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t`${t.second} ${t.minute} ${t.hour} * * *`,\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tutcOffset\n\t\t\t);\n\n\t\t\t// tick 1 sec before an hour\n\t\t\tclock.tick(1000 * 60 * 60 - 1);\n\t\t\texpect(callback).toHaveBeenCalledTimes(0);\n\n\t\t\tclock.tick(1);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run a job using cron syntax with numeric format utcOffset with minute support', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\t// current time\n\t\t\tconst t = DateTime.local();\n\n\t\t\t// uTC Offset decreased by 45 minutes\n\t\t\tconst utcOffset = t.offset - 45;\n\t\t\tconst job = new CronJob(\n\t\t\t\t`${t.second} ${t.minute} ${t.hour} * * *`,\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tutcOffset\n\t\t\t);\n\n\t\t\t// tick 1 sec before 45 minutes\n\t\t\tclock.tick(1000 * 45 * 60 - 1);\n\t\t\texpect(callback).toHaveBeenCalledTimes(0);\n\n\t\t\tclock.tick(1);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should run a job using cron syntax with number format utcOffset that is 0', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\t0\n\t\t\t);\n\n\t\t\tclock.tick(999);\n\t\t\texpect(callback).toHaveBeenCalledTimes(0);\n\n\t\t\tclock.tick(1);\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should be able to detect out of range days of month', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * 32 FEB *');\n\t\t\t}).toThrow();\n\t\t});\n\t});\n\n\tdescribe('setTime', () => {\n\t\tit('should start, change time, start again', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\n\t\t\tjob.start();\n\t\t\tclock.tick(1000);\n\n\t\t\tconst time = new CronTime('*/2 * * * * *');\n\t\t\tjob.setTime(time);\n\n\t\t\tclock.tick(4000);\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(3);\n\t\t});\n\n\t\tit('should start, stop, change time, not start again', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\n\t\t\tjob.start();\n\t\t\tclock.tick(1000);\n\n\t\t\tjob.stop();\n\t\t\tconst time = new CronTime('*/2 * * * * *');\n\t\t\tjob.setTime(time);\n\n\t\t\tclock.tick(4000);\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should setTime with invalid object', () => {\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\t\t\texpect(() => {\n\t\t\t\t// @ts-expect-error time parameter cannot be undefined\n\t\t\t\tjob.setTime(undefined);\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('should start, change time, exception', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\n\t\t\tconst time = new Date();\n\t\t\tjob.start();\n\n\t\t\tclock.tick(1000);\n\n\t\t\texpect(() => {\n\t\t\t\t// @ts-expect-error time parameter but be an instance of CronTime\n\t\t\t\tjob.setTime(time);\n\t\t\t}).toThrow();\n\n\t\t\tjob.stop();\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t});\n\n\t\tit('should create recurring job, setTime with actual date, start and run once (#739)', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\n\t\t\tconst job = new CronJob('0 0 20 * * *', callback);\n\n\t\t\tconst startDate = new Date(Date.now() + 5000);\n\t\t\tjob.setTime(new CronTime(startDate));\n\n\t\t\tjob.start();\n\n\t\t\tclock.tick(5000);\n\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t\tclock.tick(60000);\n\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\t\texpect(job.isActive).toBe(false);\n\t\t});\n\t});\n\n\tdescribe('nextDate(s)', () => {\n\t\tit('should give the next date to run at', () => {\n\t\t\tsinon.useFakeTimers();\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\t\t\tconst d = Date.now();\n\n\t\t\texpect(job.nextDate().toMillis()).toEqual(d + 1000);\n\t\t});\n\n\t\tit('should give the next 5 dates to run at', () => {\n\t\t\tsinon.useFakeTimers();\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\t\t\tconst d = Date.now();\n\n\t\t\texpect(job.nextDates(5).map(d => d.toMillis())).toEqual([\n\t\t\t\td + 1000,\n\t\t\t\td + 2000,\n\t\t\t\td + 3000,\n\t\t\t\td + 4000,\n\t\t\t\td + 5000\n\t\t\t]);\n\t\t});\n\n\t\tit('should give an empty array when called without argument', () => {\n\t\t\tconst job = new CronJob('* * * * * *', callback);\n\t\t\texpect(job.nextDates()).toHaveLength(0);\n\t\t});\n\t});\n\n\tit('should automatically setup a new timeout if we roll past the max timeout delay', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst d = new Date();\n\t\td.setMilliseconds(2147485647 * 2); // mAXDELAY in `job.js` + 2000.\n\t\tconst job = new CronJob(d, callback);\n\t\tjob.start();\n\t\tclock.tick(2147483648);\n\t\texpect(callback).toHaveBeenCalledTimes(0);\n\t\tclock.tick(2147489648);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tjob.stop();\n\t});\n\n\tit('should give the correct last execution date', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst job = new CronJob('* * * * * *', callback);\n\t\tjob.start();\n\t\tclock.tick(1000);\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\texpect(job.lastDate()?.getTime()).toBe(1000);\n\t\tjob.stop();\n\t});\n\n\tit('should give the correct last execution date for intervals greater than 25 days (#710)', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\n\t\tconst job = new CronJob('0 0 0 1 * *', callback); // at 00:00 on day-of-month 1.\n\t\tjob.start();\n\n\t\t// tick one tick before nextDate()\n\t\tclock.tick(job.nextDate().toMillis() - 1);\n\n\t\texpect(callback).toHaveBeenCalledTimes(0);\n\t\texpect(job.lastDate()?.getTime()).toBeUndefined();\n\n\t\tjob.stop();\n\t});\n\n\tit('should throw when providing both exclusive parameters timeZone and utcOffset', () => {\n\t\texpect(() => {\n\t\t\t// @ts-expect-error testing runtime exception\n\t\t\tnew CronJob(\n\t\t\t\t`* * * * *`,\n\t\t\t\tfunction () {},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\t'America/Chicago',\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\t120\n\t\t\t);\n\t\t}).toThrow();\n\t});\n\n\tit('should throw when providing both exclusive parameters timeZone and utcOffset using the object constructor', () => {\n\t\texpect(() => {\n\t\t\t// @ts-expect-error testing runtime exception\n\t\t\tCronJob.from({\n\t\t\t\tcronTime: '* * * * * *',\n\t\t\t\tonTick: function () {},\n\t\t\t\ttimeZone: 'America/Chicago',\n\t\t\t\tutcOffset: 120\n\t\t\t});\n\t\t}).toThrow();\n\t});\n\n\tit('should support async callback', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst job = new CronJob(\n\t\t\t'* * * * * *',\n\t\t\tasync function () {\n\t\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}, 500);\n\t\t\t\t});\n\t\t\t},\n\t\t\tnull,\n\t\t\ttrue\n\t\t);\n\t\tclock.tick(1500);\n\t\tjob.stop();\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t});\n\n\tit('should catch errors every time, if errorHandler is provided', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst errorFunc = jest.fn().mockImplementation(() => {\n\t\t\tthrow Error('Exception');\n\t\t});\n\t\tconst handlerFunc = jest.fn();\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: errorFunc,\n\t\t\terrorHandler: handlerFunc,\n\t\t\tstart: true\n\t\t});\n\t\tclock.tick(1000);\n\t\texpect(errorFunc).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));\n\t\tclock.tick(1000);\n\t\texpect(errorFunc).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));\n\n\t\tjob.stop();\n\t});\n\n\tit('should catch errors in async onTick, if errorHandler is provided', async () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst errorFunc = jest.fn().mockImplementation(async () => {\n\t\t\tthrow Error('Exception');\n\t\t});\n\t\tconst handlerFunc = jest.fn();\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: errorFunc,\n\t\t\terrorHandler: handlerFunc,\n\t\t\tstart: true\n\t\t});\n\t\tawait clock.tickAsync(1000);\n\t\texpect(errorFunc).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));\n\t\tawait clock.tickAsync(1000);\n\t\texpect(errorFunc).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));\n\n\t\tjob.stop();\n\n\t\tconst errorFunc2 = jest.fn().mockImplementation(async () => {\n\t\t\tthrow Error('Exception');\n\t\t});\n\t\tconst handlerFunc2 = jest.fn();\n\t\tconst job2 = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: errorFunc2,\n\t\t\terrorHandler: handlerFunc2,\n\t\t\tstart: true,\n\t\t\twaitForCompletion: true\n\t\t});\n\t\tawait clock.tickAsync(1000);\n\t\texpect(errorFunc2).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc2).toHaveBeenCalledTimes(1);\n\t\texpect(handlerFunc2).toHaveBeenLastCalledWith(new Error('Exception'));\n\t\tawait clock.tickAsync(1000);\n\t\texpect(errorFunc2).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc2).toHaveBeenCalledTimes(2);\n\t\texpect(handlerFunc2).toHaveBeenLastCalledWith(new Error('Exception'));\n\n\t\tjob2.stop();\n\t});\n\n\tit('should log errors if errorHandler is NOT provided', () => {\n\t\tconst errorFunc = jest.fn().mockImplementation(() => {\n\t\t\tthrow Error('Exception');\n\t\t});\n\t\tconst errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n\t\tCronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: errorFunc,\n\t\t\trunOnInit: true\n\t\t});\n\t\texpect(errorSpy).toHaveBeenCalled();\n\t\terrorSpy.mockRestore();\n\t});\n\n\tdescribe('waitForCompletion and job status tracking', () => {\n\t\tit('should wait for async job completion when waitForCompletion is true', async () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tlet isJobCompleted = false;\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'*/2 * * * * *',\n\t\t\t\tasync () => {\n\t\t\t\t\texpect(job.isCallbackRunning).toBe(true);\n\t\t\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tcallback();\n\t\t\t\t\t\t\tisJobCompleted = true;\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\ttrue // waitForCompletion: true\n\t\t\t);\n\n\t\t\texpect(job.isCallbackRunning).toBe(false);\n\n\t\t\t// first execution\n\t\t\tawait clock.tickAsync(2000);\n\t\t\texpect(job.isCallbackRunning).toBe(true);\n\n\t\t\t// wait for job completion\n\t\t\tawait clock.tickAsync(500);\n\t\t\texpect(isJobCompleted).toBe(false);\n\t\t\texpect(job.isCallbackRunning).toBe(true);\n\n\t\t\tawait clock.tickAsync(1000);\n\t\t\texpect(isJobCompleted).toBe(true);\n\t\t\texpect(job.isCallbackRunning).toBe(false);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should not wait for job completion when waitForCompletion is false', () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tlet isJobCompleted = false;\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\t() => {\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t\tisJobCompleted = true;\n\t\t\t\t\t}, 500);\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\tfalse // waitForCompletion: false\n\t\t\t);\n\n\t\t\texpect(job.isCallbackRunning).toBe(false);\n\n\t\t\t// first execution\n\t\t\tclock.tick(1000);\n\t\t\texpect(isJobCompleted).toBe(false);\n\t\t\texpect(job.isCallbackRunning).toBe(false);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should track multiple running jobs correctly', async () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tconst executionOrder: number[] = [];\n\t\t\tlet started = 0;\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tasync () => {\n\t\t\t\t\tstarted++;\n\t\t\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tcallback();\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t}, 1500);\n\t\t\t\t\t});\n\t\t\t\t\tconst jobNumber = executionOrder.length + 1;\n\t\t\t\t\texecutionOrder.push(jobNumber);\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\ttrue // waitForCompletion: true\n\t\t\t);\n\n\t\t\tawait clock.tickAsync(3500);\n\n\t\t\texpect(started).toBe(2);\n\t\t\texpect(executionOrder).toEqual([1]);\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should handle stop() correctly while job is running', async () => {\n\t\t\tconst clock = sinon.useFakeTimers();\n\t\t\tlet isJobCompleted = false;\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'* * * * * *',\n\t\t\t\tasync () => {\n\t\t\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tcallback();\n\t\t\t\t\t\t\tisJobCompleted = true;\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t}, 500);\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\tnull,\n\t\t\t\tfalse,\n\t\t\t\ttrue // waitForCompletion: true\n\t\t\t);\n\n\t\t\tawait clock.tickAsync(1000);\n\t\t\texpect(job.isCallbackRunning).toBe(true);\n\n\t\t\tjob.stop();\n\t\t\tawait clock.tickAsync(500);\n\n\t\t\texpect(isJobCompleted).toBe(true);\n\t\t\texpect(job.isCallbackRunning).toBe(false);\n\t\t\texpect(job.isActive).toBe(false);\n\t\t});\n\t});\n\n\tdescribe('Daylight Saving Time', () => {\n\t\t// https://github.com/kelektiv/node-cron/issues/919\n\t\tit('should not get into an infinite loop on Lisbon DST forward jump', () => {\n\t\t\tconst d = DateTime.fromISO('2024-03-30T00:59:59', {\n\t\t\t\tzone: 'Europe/Lisbon'\n\t\t\t}).toJSDate();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'0 1 30 3 *',\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\t'Europe/Lisbon'\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should not get into an infinite loop on Paris DST forward jump', () => {\n\t\t\tconst d = DateTime.fromISO('2024-03-31T01:59:59', {\n\t\t\t\tzone: 'Europe/Paris'\n\t\t\t}).toJSDate();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'0 2 31 3 *',\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\t'Europe/Paris'\n\t\t\t);\n\n\t\t\tclock.tick(1000);\n\t\t\texpect(callback).toHaveBeenCalledTimes(1);\n\n\t\t\tjob.stop();\n\t\t});\n\n\t\tit('should still execute at the desired interval when the time changes back one hour', () => {\n\t\t\t// there are two instances of 2 am. Setting to an earlier time so it is not ambiguous\n\t\t\t// see https://moment.github.io/luxon/#/zones?id=ambiguous-times\n\t\t\tconst d = DateTime.fromISO('2024-04-07T01:45:00.000', {\n\t\t\t\tzone: 'Australia/Melbourne'\n\t\t\t}).toJSDate();\n\t\t\tconst clock = sinon.useFakeTimers(d.getTime());\n\n\t\t\tconst job = new CronJob(\n\t\t\t\t'*/30 * * * *',\n\t\t\t\tcallback,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\t'Australia/Melbourne'\n\t\t\t);\n\n\t\t\tclock.tick(1000 * 60 * 60 * 3);\n\n\t\t\texpect(callback).toHaveBeenCalledTimes(6);\n\n\t\t\tjob.stop();\n\t\t});\n\t});\n});\n"
  },
  {
    "path": "tests/crontime.test.ts",
    "content": "import { DateTime } from 'luxon';\nimport sinon from 'sinon';\nimport { CronTime, validateCronExpression } from '../src';\nimport { CronError } from '../src/errors';\n\ndescribe('crontime', () => {\n\tafterEach(() => {\n\t\texpect.hasAssertions();\n\t\tsinon.restore();\n\t});\n\n\tit('should test stars (* * * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test digit (0 * * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test multi digits (08 * * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('08 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test all digits (08 8 8 8 8 5)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('08 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test too many digits (08 8 8 8 8 5)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('08 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test standard cron format (* * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test standard cron format (8 8 8 8 5)', () => {\n\t\tconst standard = new CronTime('8 8 8 8 5');\n\t\tconst extended = new CronTime('0 8 8 8 8 5');\n\n\t\t// @ts-expect-error deleting for comparaison purposes\n\t\tdelete standard.source;\n\t\t// @ts-expect-error deleting for comparaison purposes\n\t\tdelete extended.source;\n\n\t\texpect(extended).toEqual(standard);\n\t});\n\n\tit('should test hyphen (0-10 * * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0-10 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test multi hyphens (0-10 0-10 * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0-10 0-10 * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test all hyphens (0-10 0-10 1-10 1-10 1-7 0-1)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0-10 0-10 1-10 1-10 1-7 0-1');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should accept all valid ranges (0-59 0-59 0-23 1-31 1-12 0-7)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0-59 0-59 0-23 1-31 1-12 0-7');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test comma (0,10 * * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0,10 * * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test multi commas (0,10 0,10 * * * *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0,10 0,10 * * * *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test all commas (0,10 0,10 1,10 1,10 1,7 0,1)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('0,10 0,10 1,10 1,10 1,7 0,1');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test alias (* * * * jan *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * jan *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test multi aliases (* * * * jan,feb *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * jan,feb *');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test all aliases (* * * * jan,feb mon,tue)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * jan,feb mon,tue');\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test unknown alias (* * * * jar *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * jar *');\n\t\t}).toThrow();\n\t});\n\n\tit('should test unknown alias - short (* * * * j *)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * j *');\n\t\t}).toThrow();\n\t});\n\n\tit('should be case-insensitive for aliases (* * * * JAN,FEB MON,TUE)', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * JAN,FEB MON,TUE', null, null);\n\t\t}).not.toThrow();\n\t});\n\n\tit('should test too few fields', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * *', null, null);\n\t\t}).toThrow();\n\t});\n\n\tit('should test too many fields', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * * * *', null, null);\n\t\t}).toThrow();\n\t});\n\n\tit('should return the same object with 0 & 7 as Sunday (except \"source\" prop)', () => {\n\t\tconst sunday0 = new CronTime('* * * * 0', null, null);\n\t\tconst sunday7 = new CronTime('* * * * 7', null, null);\n\n\t\t// @ts-expect-error deleting for comparison purposes\n\t\tdelete sunday0.source;\n\t\t// @ts-expect-error deleting for comparison purposes\n\t\tdelete sunday7.source;\n\n\t\texpect(sunday7).toEqual(sunday0);\n\t});\n\n\tdescribe('should test out of range values', () => {\n\t\tit('should test out of range minute', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('-1 * * * *', null, null);\n\t\t\t}).toThrow();\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('60 * * * *', null, null);\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('should test out of range hour', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* -1 * * *', null, null);\n\t\t\t}).toThrow();\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* 24 * * *', null, null);\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('should test out of range day-of-month', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * 0 * *', null, null);\n\t\t\t}).toThrow();\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * 32 * *', null, null);\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('should test out of range month', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * 0 *', null, null);\n\t\t\t}).toThrow();\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * 13 *', null, null);\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('should test out of range day-of-week', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * * -1', null, null);\n\t\t\t}).toThrow();\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * * 8', null, null);\n\t\t\t}).toThrow();\n\t\t});\n\t});\n\n\tit('should test invalid wildcard expression', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * * 0*');\n\t\t}).toThrow();\n\t});\n\n\tit('should test invalid step', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * 1/ *');\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * 1/0 *');\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronTime('* * * 1/00 *');\n\t\t}).toThrow();\n\t});\n\n\tit('should test invalid range', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* 2-1 * * *');\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronTime('* 2-0 * * *');\n\t\t}).toThrow();\n\n\t\texpect(() => {\n\t\t\tnew CronTime('* 2- * * *');\n\t\t}).toThrow();\n\t});\n\n\tit('should test Date', () => {\n\t\tconst d = new Date();\n\t\tconst ct = new CronTime(d);\n\t\texpect(ct.source).toBeInstanceOf(DateTime);\n\t\texpect((ct.source as DateTime).toMillis()).toEqual(d.getTime());\n\t});\n\n\tit('should test day roll-over', () => {\n\t\tconst numHours = 24;\n\t\tconst ct = new CronTime('0 0 17 * * *');\n\n\t\tfor (let hr = 0; hr < numHours; hr++) {\n\t\t\tconst start = new Date(2012, 3, 16, hr, 30, 30);\n\t\t\tconst next = ct.getNextDateFrom(start);\n\t\t\texpect(next.toMillis() - start.getTime()).toBeLessThan(\n\t\t\t\t24 * 60 * 60 * 1000\n\t\t\t);\n\t\t\texpect(next.toMillis()).toBeGreaterThan(start.getTime());\n\t\t}\n\t});\n\n\tit('should test illegal repetition syntax', () => {\n\t\texpect(() => {\n\t\t\tnew CronTime('* * /4 * * *');\n\t\t}).toThrow();\n\t});\n\n\tit('should test next date', () => {\n\t\tconst ct = new CronTime('0 0 */4 * * *');\n\n\t\tconst nextDate = new Date();\n\t\tnextDate.setHours(23);\n\t\tconst nextdt = ct.getNextDateFrom(nextDate);\n\n\t\texpect(nextdt.toMillis()).toBeGreaterThan(nextDate.getTime());\n\t\texpect(nextdt.hour % 4).toBe(0);\n\t});\n\n\tit('should throw an exception because next date is invalid', () => {\n\t\tconst ct = new CronTime('0 0 * * * *');\n\t\tconst nextDate = new Date('My invalid date string');\n\n\t\texpect(() => {\n\t\t\tct.getNextDateFrom(nextDate);\n\t\t}).toThrow('ERROR: You specified an invalid date.');\n\t});\n\n\tit('should test next real date', () => {\n\t\tconst initialDate = new Date();\n\t\tinitialDate.setDate(initialDate.getDate() + 1); // in other case date will be in the past\n\t\tconst ct = new CronTime(initialDate);\n\n\t\tconst nextDate = new Date();\n\t\tnextDate.setMonth(nextDate.getMonth() + 1);\n\t\texpect(ct.source).toBeInstanceOf(DateTime);\n\t\texpect(nextDate.getTime()).toBeGreaterThan(\n\t\t\t(ct.source as DateTime).toMillis()\n\t\t);\n\t\tconst nextDt = ct.sendAt();\n\t\t// there shouldn't be a \"next date\" when using a real date.\n\t\t// execution happens once\n\t\t// so the return should be the date passed in unless explicitly reset\n\t\texpect(nextDt.toMillis() < nextDate.getTime()).toBeTruthy();\n\t\texpect(nextDt.toMillis()).toEqual(initialDate.getTime());\n\t});\n\n\tdescribe('presets', () => {\n\t\tit('should parse @secondly', () => {\n\t\t\tconst cronTime = new CronTime('@secondly');\n\t\t\texpect(cronTime.toString()).toBe('* * * * * *');\n\t\t});\n\n\t\tit('should parse @minutely', () => {\n\t\t\tconst cronTime = new CronTime('@minutely');\n\t\t\texpect(cronTime.toString()).toBe('0 * * * * *');\n\t\t});\n\n\t\tit('should parse @hourly', () => {\n\t\t\tconst cronTime = new CronTime('@hourly');\n\t\t\texpect(cronTime.toString()).toBe('0 0 * * * *');\n\t\t});\n\n\t\tit('should parse @daily', () => {\n\t\t\tconst cronTime = new CronTime('@daily');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 * * *');\n\t\t});\n\n\t\tit('should parse @weekly', () => {\n\t\t\tconst cronTime = new CronTime('@weekly');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 * * 0');\n\t\t});\n\n\t\tit('should parse @weekdays', () => {\n\t\t\tconst cronTime = new CronTime('@weekdays');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 * * 1,2,3,4,5');\n\t\t});\n\n\t\tit('should parse @weekends', () => {\n\t\t\tconst cronTime = new CronTime('@weekends');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 * * 0,6');\n\t\t});\n\n\t\tit('should parse @monthly', () => {\n\t\t\tconst cronTime = new CronTime('@monthly');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 1 * *');\n\t\t});\n\n\t\tit('should parse @yearly', () => {\n\t\t\tconst cronTime = new CronTime('@yearly');\n\t\t\texpect(cronTime.toString()).toBe('0 0 0 1 1 *');\n\t\t});\n\t});\n\n\tdescribe('should throw an exception because `L` not supported', () => {\n\t\tit('(* * * L * *)', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * L * *');\n\t\t\t}).toThrow();\n\t\t});\n\n\t\tit('(* * * * * L)', () => {\n\t\t\texpect(() => {\n\t\t\t\tnew CronTime('* * * * * L');\n\t\t\t}).toThrow();\n\t\t});\n\t});\n\n\tit('should strip off millisecond', () => {\n\t\tconst cronTime = new CronTime('0 */10 * * * *');\n\t\tconst x = cronTime.getNextDateFrom(new Date('2018-08-10T02:20:00.999Z'));\n\t\texpect(x.toMillis()).toEqual(\n\t\t\tnew Date('2018-08-10T02:30:00.000Z').getTime()\n\t\t);\n\t});\n\n\tit('should strip off millisecond (2)', () => {\n\t\tconst cronTime = new CronTime('0 */10 * * * *');\n\t\tconst x = cronTime.getNextDateFrom(new Date('2018-08-10T02:19:59.999Z'));\n\t\texpect(x.toMillis()).toEqual(\n\t\t\tnew Date('2018-08-10T02:20:00.000Z').getTime()\n\t\t);\n\t});\n\n\tit('should expose getNextDateFrom as a public function', () => {\n\t\tconst cronTime = new CronTime('0 */10 * * * *');\n\t\tcronTime.getNextDateFrom = jest.fn();\n\n\t\tconst testDate = new Date('2018-08-10T02:19:59.999Z');\n\t\tconst testTimezone = 'Asia/Amman';\n\t\tcronTime.getNextDateFrom(testDate, testTimezone);\n\n\t\texpect(cronTime.getNextDateFrom).toHaveBeenCalledWith(\n\t\t\ttestDate,\n\t\t\ttestTimezone\n\t\t);\n\t});\n\n\tit('should generate the right next days when cron is set to every minute', () => {\n\t\tconst cronTime = new CronTime('* * * * *');\n\t\tconst min = 60000;\n\t\tlet previousDate = DateTime.fromMillis(Date.UTC(2018, 5, 3, 0, 0));\n\t\tfor (let i = 0; i < 25; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(previousDate);\n\t\t\texpect(nextDate.valueOf()).toEqual(previousDate.valueOf() + min);\n\t\t\tpreviousDate = nextDate;\n\t\t}\n\t});\n\n\tit('should generate the right next days when cron is set to every 15 min', () => {\n\t\tconst cronTime = new CronTime('*/15 * * * *');\n\t\tconst min = 60000 * 15;\n\t\tlet previousDate = DateTime.fromMillis(Date.UTC(2016, 6, 3, 0, 0));\n\t\tfor (let i = 0; i < 25; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(previousDate);\n\t\t\texpect(nextDate.valueOf()).toEqual(previousDate.valueOf() + min);\n\t\t\tpreviousDate = nextDate;\n\t\t}\n\t});\n\tit('should work around offset changes that shift time back (1)', () => {\n\t\tconst d = new Date('10-7-2018');\n\t\t// america/Sao_Paulo has a offset change around NOV 3 2018.\n\t\tconst cronTime = new CronTime('0 0 9 4 * *');\n\t\tconst nextDate = cronTime.getNextDateFrom(d, 'America/Sao_Paulo');\n\t\texpect(nextDate.setZone('America/Sao_Paulo').toISO()).toEqual(\n\t\t\t'2018-11-04T09:00:00.000-02:00'\n\t\t);\n\t});\n\tit('should work around offset changes that shift time back (2)', () => {\n\t\t// asia/Amman DST ends in  26 - OCT-2018 (-1 to hours)\n\t\tconst currentDate = DateTime.fromISO('2018-10-25T23:00', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('0 0 * * *');\n\t\tconst nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\tconst expectedDate = DateTime.fromISO('2018-10-26T00:00+03:00', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\texpect(nextDate.toMillis() - expectedDate.toMillis()).toBe(0);\n\t});\n\tit('should work around offset changes that shifts time forward', () => {\n\t\t// asia/Amman DST starts in  30-March-2018 (+1 to hours)\n\t\tlet currentDate = DateTime.fromISO('2018-03-29T23:00', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('* * * * *');\n\t\tfor (let i = 0; i < 100; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(1000 * 60);\n\t\t\tcurrentDate = nextDate;\n\t\t}\n\t});\n\tit('should not execute immediately if conditions have not been met during forward DST jump', () => {\n\t\t// europe/Paris DST starts at 30 Mar 2025, 02:00 (+1 to hours)\n\t\tlet currentDate = DateTime.fromISO('2025-03-30T01:59', {\n\t\t\tzone: 'Europe/Paris'\n\t\t});\n\t\tconst cronTime = new CronTime('20 4 * * *');\n\t\tconst nextDate = cronTime.getNextDateFrom(currentDate, 'Europe/Paris');\n\t\texpect(nextDate.toString()).toEqual(\n\t\t\tDateTime.fromISO('2025-03-30T04:20', { zone: 'Europe/Paris' }).toString()\n\t\t);\n\t});\n\tit('Should schedule jobs inside offset changes when started exactly one month before, for monthly jobs', () => {\n\t\t// there is a DST jump on March 9 at midnight\n\t\tlet currentDate = DateTime.fromISO('2025-02-09T00:30:00', {\n\t\t\tzone: 'Cuba'\n\t\t});\n\t\tconst cronTime = new CronTime('0 0 9 * *');\n\t\tlet nextDate = cronTime.getNextDateFrom(currentDate, 'Cuba');\n\t\texpect(nextDate.toISO()).toEqual(\n\t\t\t// 28 days minus half an hour since we jump forward\n\t\t\tDateTime.fromMillis(\n\t\t\t\tcurrentDate.toMillis() + 1000 * 60 * 60 * 24 * 28 - 1000 * 60 * 30,\n\t\t\t\t{ zone: 'Cuba' }\n\t\t\t).toISO()\n\t\t);\n\t});\n\tit('Should schedule jobs inside offset changes that shifts time forward to the end of the shift, for weekly jobs', () => {\n\t\tlet currentDate = DateTime.fromISO('2018-03-29T23:15', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('30 0 * * 5'); // the next 0:30 is March 30th, but it will jump from 0:00 to 1:00.\n\t\tlet nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t1000 * 60 * 45\n\t\t); // 45 minutes is 30T00:00, which jumps to 1:00 which is past the trigger of 0:30.\n\t\t// the next one should just be at 0:30 again. i.e. a week minus 30 minutes.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t3600000 * 24 * 7 - 60000 * 30\n\t\t);\n\t\t// the next one is again at 0:30, but now we're 'back to normal' with weekly offsets.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t1000 * 3600 * 24 * 7\n\t\t);\n\t});\n\tit('Should schedule jobs inside offset changes that shifts the time forward to the end of the shift, for daily jobs', () => {\n\t\tlet currentDate = DateTime.fromISO('2018-03-29T23:45', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('30 0 * * *'); // the next 0:30 is March 30th, but it will jump from 0:00 to 1:00.\n\t\tlet nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t1000 * 60 * 15\n\t\t); // 15 minutes is 30T00:00, which jumps to 1:00 which is past the trigger of 0:30.\n\t\t// the next one is tomorrow at 0:30, so 23h30m.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t1000 * 3600 * 24 - 1000 * 60 * 30\n\t\t);\n\t\t// back to normal.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t1000 * 3600 * 24\n\t\t);\n\t});\n\tit('Should schedule jobs inside offset changes that shifts the time forward to the end of the shift, for hourly jobs', () => {\n\t\tlet currentDate = DateTime.fromISO('2018-03-29T23:45', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('30 * * * *'); // the next 0:30 is March 30th, but it will jump from 0:00 to 1:00.\n\t\tlet nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\t// 15 minutes is 30T00:00, which jumps to 1:00 which is past the trigger of 0:30.\n\t\texpect(nextDate.toISO()).toEqual(\n\t\t\tcurrentDate.plus({ milliseconds: 1000 * 60 * 15 }).toISO()\n\t\t);\n\t\t// the next one is at 1:30, so 30m.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toISO()).toEqual(\n\t\t\tcurrentDate.plus({ milliseconds: 1000 * 60 * 30 }).toISO()\n\t\t);\n\t\t// back to normal.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toISO()).toEqual(\n\t\t\tcurrentDate.plus({ milliseconds: 1000 * 60 * 60 }).toISO()\n\t\t);\n\t});\n\tit('Should schedule jobs inside offset changes that shifts the time forward to the end of the shift, for minutely jobs', () => {\n\t\tlet currentDate = DateTime.fromISO('2018-03-29T23:59', {\n\t\t\tzone: 'Asia/Amman'\n\t\t});\n\t\tconst cronTime = new CronTime('* * * * *'); // the next minute is 0:00 is March 30th, but it will jump from 0:00 to 1:00.\n\t\tlet nextDate = cronTime.getNextDateFrom(currentDate, 'Asia/Amman');\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(1000 * 60);\n\t\t// the next one is at 1:01:00, this should still be 60 seconds in the future.\n\t\tcurrentDate = nextDate;\n\t\tnextDate = cronTime.getNextDateFrom(currentDate);\n\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(1000 * 60);\n\t});\n\tit('should generate the right N next days for * * * * *', () => {\n\t\tconst cronTime = new CronTime('* * * * *');\n\t\tlet currentDate = DateTime.local().set({ second: 0, millisecond: 0 });\n\t\tfor (let i = 0; i < 100; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(currentDate);\n\t\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(1000 * 60);\n\t\t\tcurrentDate = nextDate;\n\t\t}\n\t});\n\tit('should generate the right  N next days for 0 0 9 * * *', () => {\n\t\tconst cronTime = new CronTime('0 0 9 * * *');\n\t\tlet currentDate = DateTime.local()\n\t\t\t.setZone('utc')\n\t\t\t.set({ hour: 9, minute: 0, second: 0, millisecond: 0 });\n\t\tfor (let i = 0; i < 100; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(currentDate);\n\t\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t\t1000 * 60 * 60 * 24\n\t\t\t);\n\t\t\tcurrentDate = nextDate;\n\t\t}\n\t});\n\tit('should generate the right  N next days for 0 0 * * * with a time zone', () => {\n\t\tconst cronTime = new CronTime('0 * * * *');\n\t\tlet currentDate = DateTime.fromISO('2018-11-02T23:00', {\n\t\t\tzone: 'America/Sao_Paulo'\n\t\t}).set({ second: 0, millisecond: 0 });\n\t\tfor (let i = 0; i < 25; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(\n\t\t\t\tcurrentDate,\n\t\t\t\t'America/Sao_Paulo'\n\t\t\t);\n\t\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t\t1000 * 60 * 60\n\t\t\t);\n\t\t\tcurrentDate = nextDate;\n\t\t}\n\t});\n\tit('should generate the right  N next days for */3 * * * * with a time zone', () => {\n\t\tconst cronTime = new CronTime('*/3 * * * *');\n\t\tlet currentDate = DateTime.fromISO('2018-11-02T23:00', {\n\t\t\tzone: 'America/Sao_Paulo'\n\t\t}).set({ second: 0, millisecond: 0 });\n\t\tfor (let i = 0; i < 25; i++) {\n\t\t\tconst nextDate = cronTime.getNextDateFrom(\n\t\t\t\tcurrentDate,\n\t\t\t\t'America/Sao_Paulo'\n\t\t\t);\n\t\t\texpect(nextDate.toMillis() - currentDate.toMillis()).toEqual(\n\t\t\t\t1000 * 60 * 3\n\t\t\t);\n\t\t\tcurrentDate = nextDate;\n\t\t}\n\t});\n\tit('should test valid range of months (*/15 * * 7-12 *)', () => {\n\t\tconst cronTime = new CronTime('*/15 * * 7-12 *');\n\t\tconst previousDate1 = new Date(Date.UTC(2018, 3, 0, 0, 0));\n\t\tconst nextDate1 = cronTime.getNextDateFrom(previousDate1, 'UTC');\n\t\texpect(nextDate1.toJSDate().toUTCString()).toEqual(\n\t\t\tnew Date(Date.UTC(2018, 6, 1, 0, 0)).toUTCString()\n\t\t);\n\t\tconst previousDate2 = new Date(Date.UTC(2018, 8, 0, 0, 0));\n\t\tconst nextDate2 = cronTime.getNextDateFrom(previousDate2, 'UTC');\n\t\texpect(nextDate2.toJSDate().toUTCString()).toEqual(\n\t\t\tnew Date(Date.UTC(2018, 8, 0, 0, 15)).toUTCString()\n\t\t);\n\t});\n\tit('should generate the right next day when cron is set to every 15 min in Feb', () => {\n\t\tconst cronTime = new CronTime('*/15 * * FEB *');\n\t\tconst previousDate = new Date(Date.UTC(2018, 3, 0, 0, 0));\n\t\tconst nextDate = cronTime.getNextDateFrom(previousDate, 'UTC');\n\t\texpect(nextDate.toISO()).toEqual('2019-02-01T00:00:00.000Z');\n\t});\n\tit('should generate the right next day when cron is set to both day of the month and day of the week (1)', () => {\n\t\tconst cronTime = new CronTime('0 8 1 * 4');\n\t\tconst previousDate = new Date(Date.UTC(2019, 3, 21, 0, 0));\n\t\tconst nextDate = cronTime.getNextDateFrom(previousDate, 'UTC');\n\t\texpect(nextDate.toMillis()).toEqual(\n\t\t\tnew Date(Date.UTC(2019, 3, 25, 8, 0)).getTime()\n\t\t);\n\t});\n\tit('should generate the right next day when cron is set to both day of the month and day of the week (2)', () => {\n\t\tconst cronTime = new CronTime('0 8 1 * 4');\n\t\tconst previousDate = new Date(Date.UTC(2019, 3, 26, 0, 0));\n\t\tconst nextDate = cronTime.getNextDateFrom(previousDate, 'UTC');\n\t\texpect(nextDate.toMillis()).toEqual(\n\t\t\tnew Date(Date.UTC(2019, 4, 1, 8, 0)).getTime()\n\t\t);\n\t});\n\tit('should generate the right next day when cron is set to both day of the month and day of the week (3)', () => {\n\t\tconst cronTime = new CronTime('0 8 1 * 4');\n\t\tconst previousDate = new Date(Date.UTC(2019, 7, 1, 7, 59));\n\t\tconst nextDate = cronTime.getNextDateFrom(previousDate, 'UTC');\n\t\texpect(nextDate.valueOf()).toEqual(\n\t\t\tnew Date(Date.UTC(2019, 7, 1, 8, 0)).valueOf()\n\t\t);\n\t});\n\n\tit('should accept 0 as a valid UTC offset', () => {\n\t\tsinon.useFakeTimers();\n\t\tconst cronTime = new CronTime('0 11 * * *', null, 0);\n\t\tconst expected = DateTime.local().plus({ hours: 11 }).toSeconds();\n\t\tconst actual = cronTime.sendAt().toSeconds();\n\t\texpect(actual).toEqual(expected);\n\t});\n\n\tit('should accept -120 as a valid UTC offset', () => {\n\t\tsinon.useFakeTimers();\n\t\tconst cronTime = new CronTime('0 11 * * *', null, -120);\n\t\tconst expected = DateTime.local().plus({ hours: 13 }).toSeconds();\n\t\tconst actual = cronTime.sendAt().toSeconds();\n\t\texpect(actual).toEqual(expected);\n\t});\n\n\tit('should detect real date in the past', () => {\n\t\tconst clock = sinon.useFakeTimers();\n\t\tconst d = new Date();\n\t\tclock.tick(1000);\n\t\tconst time = new CronTime(d);\n\t\texpect(() => {\n\t\t\ttime.sendAt();\n\t\t}).toThrow();\n\t});\n\n\tit('should throw when providing both exclusive parameters timeZone and utcOffset', () => {\n\t\texpect(() => {\n\t\t\t// @ts-expect-error testing runtime exception\n\t\t\tnew CronTime('* * * * *', 'Asia/Amman', 120);\n\t\t}).toThrow();\n\t});\n});\n\ndescribe('validateCronExpression', () => {\n\tit('should return true for valid cron expressions', () => {\n\t\tconst validExpressions = [\n\t\t\t'* * * * *',\n\t\t\t'0 0 * * *',\n\t\t\t'0 0 1 1 *',\n\t\t\t'*/5 * * * *'\n\t\t];\n\n\t\tvalidExpressions.forEach(expression => {\n\t\t\tconst validation = validateCronExpression(expression);\n\t\t\texpect(validation.valid).toBe(true);\n\t\t\texpect(validation.error).toBeUndefined();\n\t\t});\n\t});\n\n\tit('should return false for invalid cron expressions', () => {\n\t\tconst invalidExpressions = [\n\t\t\t'* * * *',\n\t\t\t'60 * * * *',\n\t\t\t'* * * * * * *',\n\t\t\t'invalid cron'\n\t\t];\n\n\t\tinvalidExpressions.forEach(expression => {\n\t\t\tconst validation = validateCronExpression(expression);\n\t\t\texpect(validation.valid).toBe(false);\n\t\t\texpect(validation.error).toBeInstanceOf(CronError);\n\t\t});\n\t});\n});\n"
  },
  {
    "path": "tests/threshold.test.ts",
    "content": "import sinon from 'sinon';\nimport { CronJob } from '../src';\n\ndescribe('threshold behavior', () => {\n\tlet callback: jest.Mock;\n\tlet warnSpy: jest.SpyInstance;\n\n\tbeforeEach(() => {\n\t\tcallback = jest.fn();\n\t\twarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});\n\t});\n\n\tafterEach(() => {\n\t\tsinon.restore();\n\t\twarnSpy.mockRestore();\n\t});\n\n\tit('should call the callback the correct amount of times', () => {\n\t\tconst EVERY = 5;\n\t\tconst TICK = EVERY * 1000;\n\t\tconst DELAY = 350;\n\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: `*/${EVERY} * * * * *`,\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 350\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(TICK)\n\t\t\t.onCall(1)\n\t\t\t.returns(-DELAY)\n\t\t\t// => 1st assertion (delay simulated)\n\t\t\t.onCall(2)\n\t\t\t.returns(TICK)\n\t\t\t.onCall(3)\n\t\t\t.returns(TICK)\n\t\t\t// => 2nd assertion (no delay simulated)\n\t\t\t.onCall(4)\n\t\t\t.returns(-DELAY)\n\t\t\t.onCall(5)\n\t\t\t.returns(TICK);\n\t\t// => 3rd assertion (delay simulated)\n\t\t// => 4th assertion (scheduled during the previous iteration)\n\n\t\tconst clock = sinon.useFakeTimers();\n\n\t\tjob.start();\n\n\t\tclock.tick(TICK);\n\t\t// 2 calls: 1 from the initial scheduled execution, 1 from the immediate execution\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\n\t\tclock.tick(TICK);\n\t\t// 1 call: no delay simulated\n\t\texpect(callback).toHaveBeenCalledTimes(3);\n\n\t\tclock.tick(TICK);\n\t\t// 2 calls: 1 from the scheduled execution, 1 from the immediate execution\n\t\texpect(callback).toHaveBeenCalledTimes(5);\n\n\t\tclock.tick(TICK);\n\t\t// 1 call: no delay simulated\n\t\texpect(callback).toHaveBeenCalledTimes(6);\n\n\t\texpect(job.isActive).toBe(true);\n\t\tjob.stop();\n\t});\n\n\tit('should execute job immediately on start if negative timeout is within threshold', () => {\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '*/5 * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 300,\n\t\t\tname: 'test-job'\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(-100)\n\t\t\t.onCall(1)\n\t\t\t.returns(5000);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 100ms');\n\t\texpect(message).toContain('Executing immediately');\n\n\t\tjob.stop();\n\t});\n\n\tit('should execute job immediately if negative timeout is within threshold', () => {\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 300,\n\t\t\tname: 'test-job'\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(1000)\n\t\t\t.onCall(1)\n\t\t\t.returns(-50);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\t// 2 calls: 1 from the initial scheduled execution, 1 from the immediate execution\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 50ms');\n\t\texpect(message).toContain('Executing immediately');\n\n\t\tjob.stop();\n\t});\n\n\tit('should skip immediate execution if negative timeout exceeds threshold', () => {\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 100,\n\t\t\tname: 'test-job'\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(1000)\n\t\t\t.onCall(1)\n\t\t\t.returns(-200);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\t// 1 call from the initial scheduled execution\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 200ms');\n\t\texpect(message).toContain('Skipping execution as it exceeds threshold');\n\n\t\tjob.stop();\n\t});\n\n\tit('should use 250ms as default threshold', () => {\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tname: 'test-job'\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(1000)\n\t\t\t.onCall(1)\n\t\t\t.returns(-250);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron-expression');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\texpect(callback).toHaveBeenCalledTimes(2);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 250ms');\n\t\texpect(message).toContain('Executing immediately');\n\n\t\tjob.stop();\n\t});\n\n\tit('should properly identify named jobs in logs', () => {\n\t\tconst jobName = 'test-named-job';\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 250,\n\t\t\tname: jobName\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(1000)\n\t\t\t.onCall(1)\n\t\t\t.returns(-500);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron-expression');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 500ms');\n\t\texpect(message).toContain(`job \"${jobName}\" with cron expression`);\n\t\texpect(message).toContain('test-cron-expression');\n\n\t\tjob.stop();\n\t});\n\n\tit('should properly identify unnamed jobs in logs', () => {\n\t\tconst job = CronJob.from({\n\t\t\tcronTime: '* * * * * *',\n\t\t\tonTick: callback,\n\t\t\tstart: false,\n\t\t\tthreshold: 250\n\t\t});\n\n\t\tsinon\n\t\t\t.stub(job.cronTime, 'getTimeout')\n\t\t\t.onCall(0)\n\t\t\t.returns(1000)\n\t\t\t.onCall(1)\n\t\t\t.returns(-500);\n\t\tsinon.stub(job.cronTime, 'source').value('test-cron-expression');\n\n\t\tconst clock = sinon.useFakeTimers();\n\t\tjob.start();\n\t\tclock.tick(1000);\n\n\t\texpect(callback).toHaveBeenCalledTimes(1);\n\t\tconst message = warnSpy.mock.calls[0][0];\n\t\texpect(message).toContain('Missed execution deadline by 500ms');\n\t\texpect(message).toContain('for job with cron expression');\n\t\texpect(message).toContain('test-cron-expression');\n\n\t\tjob.stop();\n\t});\n});\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n\t\"extends\": \"./tsconfig.json\",\n\t\"exclude\": [\"node_modules\", \"tests\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ES2015\",\n\t\t\"module\": \"commonjs\",\n\t\t\"outDir\": \"./dist\",\n\t\t\"incremental\": true,\n\t\t\"declaration\": true,\n\t\t\"removeComments\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"allowSyntheticDefaultImports\": true,\n\t\t\"sourceMap\": false,\n\t\t\"noErrorTruncation\": true,\n\t\t\"resolveJsonModule\": true,\n\n\t\t/**\n\t\t * strictest Typescript possible\n\t\t */\n\t\t\"strict\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"noImplicitReturns\": true,\n\t\t\"noPropertyAccessFromIndexSignature\": true,\n\t\t\"noImplicitOverride\": true,\n\t\t\"exactOptionalPropertyTypes\": true,\n\t\t\"noUncheckedIndexedAccess\": true,\n\t\t\"allowUnusedLabels\": false,\n\t\t\"allowUnreachableCode\": false,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"strictPropertyInitialization\": true\n\t}\n}\n"
  }
]