npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guzelbaspinar/periodic-runner

v0.2.0

Published

A non-overlapping, self-rescheduling periodic task runner with active-hours, weekday, and holiday support (CJS, ESM, and TypeScript compatible).

Readme

periodic-runner

A setTimeout-based, non-overlapping self-rescheduling periodic task runner. Unlike setInterval, the next run is never triggered before the previous one finishes, because the next setTimeout is scheduled only after the task has been awaited.

Fully compatible with CommonJS, ESM, and TypeScript.

Published on npm as @guzelbaspinar/periodic-runner.

Features

  • ✅ Overlap protection (a new run never starts before the previous one finishes)
  • ✅ Active time window support (activeHours) — including windows that wrap past midnight (e.g. 22:00-06:00)
  • ✅ Restrict to specific days of the week (weekDays)
  • ✅ Skip holiday dates (holidays), updatable at runtime, accepts an array or a Set
  • ✅ IANA timezone support
  • ✅ Custom error handling (onError) and custom logger injection
  • ✅ Optional per-task watchdog (taskTimeoutMs) — reports hung tasks via onError instead of locking up forever
  • ✅ CJS + ESM + TypeScript types in a single package (dist/index.cjs, dist/index.js, dist/index.d.ts)

Installation

npm install @guzelbaspinar/periodic-runner

Usage

TypeScript / ESM

import { PeriodicRunner } from '@guzelbaspinar/periodic-runner';

const runner = new PeriodicRunner({
  name: 'InitialCache',
  period: 7000,
  task: async () => {
    // work to run periodically
  },
  activeHours: { start: '09:50', end: '18:30' },
  weekDays: [1, 2, 3, 4, 5], // weekdays only (0=Sunday ... 6=Saturday)
  holidays: new Set(['2026-01-01', '2026-04-23']),
  timezone: 'Europe/Istanbul',
  onError: (err) => console.error('task failed:', err),
});

await runner.start();

// ... later
runner.stop();

CommonJS

const { PeriodicRunner } = require('@guzelbaspinar/periodic-runner');

const runner = new PeriodicRunner({
  period: 5000,
  task: async () => { /* ... */ },
});

runner.start();

See EXAMPLES.md for full CJS, ESM, and TypeScript examples.

Managing holidays dynamically

The holiday list is not static; it can be updated at any time from an external source (e.g. data fetched periodically from a public holiday API). It accepts either an Array or a Set of "YYYY-MM-DD" strings:

runner.setHolidays(['2026-01-01', '2026-05-01', '2026-05-19']); // replaces the whole list, array or Set
runner.addHoliday('2026-08-30');                                 // adds a single day
runner.removeHoliday('2026-08-30');                              // removes a single day
runner.getHolidays();                                            // -> Set<string>

Note: the holiday check is evaluated against the current date (computed according to the timezone option) on every tick, so simply keeping the list up to date is enough — no extra "valid from/to" logic is needed.

Task watchdog (taskTimeoutMs)

By default the runner waits indefinitely for task() to settle; a task that never resolves/rejects (e.g. a hung HTTP request) leaves isRunning true forever and every subsequent tick is silently skipped. Set taskTimeoutMs to bound how long a single run is allowed to take:

const runner = new PeriodicRunner({
  period: 5000,
  taskTimeoutMs: 3000, // treat the tick as failed if task() hasn't settled within 3s
  task: async () => {
    await fetch('https://example.com', { signal: AbortSignal.timeout(3000) });
  },
  onError: (err) => console.error('task timed out or failed:', err),
});

Note: the task itself is not cancelled — Promises cannot be aborted from the outside. taskTimeoutMs only stops the runner from waiting on it, so isRunning unlocks and the next tick isn't skipped forever; onError receives a timeout error. If your task can hang (e.g. on network I/O), pair taskTimeoutMs with your own cancellation (like AbortSignal.timeout) inside the task for full cleanup.

API

new PeriodicRunner(options)

| Field | Type | Required | Description | |---|---|---|---| | task | () => Promise<void> \| void | ✅ | Function executed on every period | | name | string | ❌ | Name shown in logs (default: "PeriodicRunner") | | period | number | ❌ | Delay between runs in ms (default: 7000) | | onError | (error: unknown) => void | ❌ | Called when task throws | | activeHours | { start: string; end: string } | ❌ | Active window in "HH:mm" format | | weekDays | number[] (0-6) | ❌ | Days of the week the task is allowed to run on (0=Sunday) | | holidays | string[] \| Set<string> ("YYYY-MM-DD") | ❌ | Dates the task must not run on | | timezone | string | ❌ | IANA timezone, e.g. "Europe/Istanbul" | | logger | { debug, error } | ❌ | Custom logger (default: console) | | taskTimeoutMs | number | ❌ | Watchdog in ms. If task doesn't settle in time, the runner reports it via onError and unlocks (see below) |

Methods

  • start(): Promise<void> — starts the loop
  • stop(): void — stops the loop
  • setHolidays(holidays: string[] | Set<string>): void
  • addHoliday(date: string): void
  • removeHoliday(date: string): void
  • getHolidays(): Set<string>
  • getWeekDays(): number[] | null

Getters

  • isRunning: boolean — whether a task is currently executing
  • isStopped: boolean — whether the runner has been stopped

Development

npm install
npm run typecheck
npm run typecheck:examples
npm test
npm run test:coverage
npm run build       # generates cjs + esm + d.ts into dist/

Contributing

Changes land on main through pull requests; the Protect main ruleset requires CI (test, coverage) to pass before merge.

License

MIT