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

@abdoseadaa/smart-interval

v1.0.1

Published

Production-grade setInterval replacement backed by BullMQ and Redis. No overlapping runs, persistence, retries, and graceful shutdown.

Readme

smart-interval

Production-grade setInterval replacement backed by BullMQ and Redis. Use it when you need recurring work that must not overlap, survive restarts, and shut down cleanly.

  • No overlapping runs — one job at a time per interval (concurrency 1)
  • Redis-backed — schedule and job state persist across process restarts
  • Retries & backoff — configurable attempts and delay on failure
  • Graceful shutdown — stop worker and queue without leaving orphan jobs
  • TypeScript — full typings and run context (every, last_run_at, next_run_at, trigger)
  • Cycle hooks — run logic after / before / middle of each interval (within the same cycle); each callback receives ctx.trigger so you know where it was invoked

Requirements

  • Node.js ≥ 18
  • Redis (any version compatible with BullMQ / ioredis)

Installation

npm i @abdoseadaa/smart-interval

Peer / runtime: BullMQ and ioredis are listed as dependencies and will be installed with the package.


Quick start

import { configureRedis, setSmartInterval } from '@abdoseadaa/smart-interval';

// 1. Configure Redis once at app startup
configureRedis({
  host: process.env.REDIS_HOST ?? 'localhost',
  port: Number(process.env.REDIS_PORT ?? 6379),
  password: process.env.REDIS_PASSWORD,
});

// 2. Start a recurring task
const handle = await setSmartInterval({
  name: 'sync-orders',
  every: 60_000, // ms
  callback: async (ctx) => {
    console.log('Running at', ctx.last_run_at, 'next at', ctx.next_run_at);
    await doWork();
    if (done) await ctx.stop();
  },
});

// 3. Graceful shutdown
process.on('SIGTERM', async () => {
  await handle.stop();
  process.exit(0);
});

API reference

configureRedis(config)

Configures the Redis connection used by all SmartInterval queues and workers. Call this once before any setSmartInterval.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | config | RedisConfig | Yes | Redis connection options. |

Returns: void

Throws: Nothing. If Redis is not configured and you call setSmartInterval, the library will throw at that time.


setSmartInterval(options)

Creates a recurring job (repeatable BullMQ job) and a worker that runs your callback on each tick. Returns a handle to control and inspect the interval.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | options | SmartIntervalOptions | Yes | Interval name, period, callback, and optional retry/hooks. |

Returns: Promise<SmartIntervalHandle>

Throws: If Redis was not configured, or on Redis/queue errors.


Types

RedisConfig

Options passed to configureRedis().

| Property | Type | Required | Description | |----------|------|----------|-------------| | host | string | Yes | Redis host (e.g. 'localhost'). | | port | number | Yes | Redis port (e.g. 6379). | | password | string | No | Redis password. | | db | number | No | Redis DB index. Default: 0. | | tls | boolean | No | Use TLS. Default: false. |


SmartIntervalOptions

Options passed to setSmartInterval().

| Property | Type | Required | Description | |----------|------|----------|-------------| | name | string | Yes | Unique name for this interval (used as BullMQ queue name). | | every | number | Yes | Interval in milliseconds (e.g. 60_000 = 1 minute). | | callback | SmartIntervalCallback | Yes | Function run on each tick; receives SmartIntervalContext. | | retries | number | No | Number of retries on failure (default: 3). Total attempts = retries + 1. | | retryDelay | number | No | Delay in ms between retries (default: 5000). | | keepCompleted | number | No | Max completed jobs to keep in Redis (default: 50). | | keepFailed | number | No | Max failed jobs to keep in Redis (default: 20). | | onError | (error: Error, jobId?: string) => void | No | Called when a job fails after all retries. | | onComplete | (jobId?: string) => void | No | Called when a job completes successfully. | | after | SmartIntervalAfterHook | No | Run within the cycle: after last_run_at by when ms. when must be in [0, every]. | | before | SmartIntervalBeforeHook | No | Run within the cycle: before next_run_at by when ms. when must be in [0, every]. | | middle | SmartIntervalMiddleHook | No | Run at the middle of the cycle (last_run_at + every/2). No when — fixed. |


SmartIntervalTrigger

Identifies which callback was invoked. Every callback (main or hook) receives the same SmartIntervalContext with a trigger field:

| Value | Meaning | |--------|--------| | 'interval' | Main interval tick (your callback). | | 'after' | Hook run after last_run_at by after.when ms. | | 'before' | Hook run before next_run_at by before.when ms. | | 'middle' | Hook run at the middle of the cycle. |

type SmartIntervalTrigger = 'interval' | 'after' | 'before' | 'middle';

SmartIntervalAfterHook / SmartIntervalBeforeHook / SmartIntervalMiddleHook

  • after: { when: number; callback: SmartIntervalCallback }when = ms after last_run_at; must be in [0, every].
  • before: { when: number; callback: SmartIntervalCallback }when = ms before next_run_at; must be in [0, every].
  • middle: { callback: SmartIntervalCallback } — runs at last_run_at + every/2 (no when).

If when is outside [0, every], setSmartInterval throws at startup.


SmartIntervalCallback

Type of the callback in SmartIntervalOptions:

type SmartIntervalCallback = (ctx: SmartIntervalContext) => Promise<void> | void;
  • Parameter: ctx — run context (handle + every, last_run_at, next_run_at, trigger).
  • Return: void or Promise<void>. Use ctx.trigger to know if the run is from the main interval or from an after/before/middle hook.

SmartIntervalContext

Object passed to your callback on every run (main interval or after/before/middle hook). It extends the control handle with run metadata and a trigger flag.

| Property | Type | Description | |----------|------|-------------| | trigger | SmartIntervalTrigger | Which callback was invoked: 'interval', 'after', 'before', or 'middle'. | | every | number | Interval in ms (same as options.every). | | last_run_at | Date \| null | When this run started (from job metadata), or null if unknown. | | next_run_at | Date \| null | Approximate next scheduled run time (current run start + every), or null. | | stop | () => Promise<void> | Stop the interval and close the worker/queue. | | pause | () => Promise<void> | Pause the repeatable schedule (jobs stay in queue). | | resume | () => Promise<void> | Resume a paused interval. | | runNow | () => Promise<void> | Enqueue one immediate run (in addition to the schedule). | | getStatus | () => Promise<SmartIntervalStatus> | Get current queue/handle status. |


SmartIntervalHandle

Return type of setSmartInterval(). Same control methods as SmartIntervalContext (no every / last_run_at / next_run_at).

| Method | Signature | Description | |--------|------------|-------------| | stop | () => Promise<void> | Stop the interval, close worker, obliterate and close queue. | | pause | () => Promise<void> | Pause the repeatable schedule. | | resume | () => Promise<void> | Resume after pause. | | runNow | () => Promise<void> | Trigger one extra run now. | | getStatus | () => Promise<SmartIntervalStatus> | Get current status. |


SmartIntervalStatus

Return type of handle.getStatus() / ctx.getStatus().

| Property | Type | Description | |----------|------|-------------| | name | string | Interval name (queue name). | | isPaused | boolean | Whether the queue is paused. | | waitingCount | number | Jobs waiting to be processed. | | activeCount | number | Jobs currently processing. | | completedCount | number | Completed jobs (up to keepCompleted). | | failedCount | number | Failed jobs (up to keepFailed). |


Examples

Basic recurring task

const handle = await setSmartInterval({
  name: 'health-check',
  every: 30_000,
  callback: async (ctx) => {
    await pingServices();
    console.log('Next run at', ctx.next_run_at);
  },
});

Use run metadata and stop from inside

await setSmartInterval({
  name: 'sync-orders',
  every: 60_000,
  callback: async (ctx) => {
    console.log('Interval (ms):', ctx.every);
    console.log('This run started:', ctx.last_run_at);
    const synced = await syncOrders();
    if (synced >= 0) await ctx.stop(); // stop when no more work
  },
});

Error and completion hooks

await setSmartInterval({
  name: 'report-generation',
  every: 3600_000,
  retries: 5,
  retryDelay: 10_000,
  callback: async (ctx) => await generateReport(),
  onError: (err, jobId) => {
    logger.error('Report job failed', { jobId, error: err.message });
  },
  onComplete: (jobId) => {
    logger.info('Report job completed', { jobId });
  },
});

Cycle hooks (after, before, middle)

Run logic at fixed points within each interval cycle. Each hook receives the same ctx with ctx.trigger set so you can branch on where the run came from.

await setSmartInterval({
  name: 'sync-with-hooks',
  every: 60_000,
  callback: async (ctx) => {
    if (ctx.trigger !== 'interval') return;
    await doMainWork();
  },
  after: {
    when: 5_000, // 5s after last_run_at
    callback: async (ctx) => {
      console.log('After hook at', ctx.last_run_at, 'trigger:', ctx.trigger); // trigger === 'after'
    },
  },
  before: {
    when: 10_000, // 10s before next_run_at
    callback: async (ctx) => {
      console.log('Before hook, next at', ctx.next_run_at, 'trigger:', ctx.trigger); // 'before'
    },
  },
  middle: {
    callback: async (ctx) => {
      console.log('Middle of cycle', ctx.trigger); // 'middle'
    },
  },
});

after.when and before.when must be in [0, every]; otherwise setSmartInterval throws.


Pause / resume and run now

const handle = await setSmartInterval({
  name: 'notifications',
  every: 5_000,
  callback: async () => await sendBatch(),
});

await handle.pause();   // stop scheduled runs
// ... later ...
await handle.resume(); // resume schedule
await handle.runNow(); // one extra run immediately
const status = await handle.getStatus();
console.log(status.waitingCount, status.activeCount);

Graceful shutdown

const intervals = [
  await setSmartInterval({ name: 'job-a', every: 1000, callback: jobA }),
  await setSmartInterval({ name: 'job-b', every: 2000, callback: jobB }),
];

process.on('SIGTERM', async () => {
  await Promise.all(intervals.map((h) => h.stop()));
  process.exit(0);
});

Scripts (development)

| Script | Description | |--------|-------------| | npm run build | Clean dist/ and compile TypeScript (emits JS + .d.ts). | | npm run pkg | Build and run npm pack --dry-run to inspect the publish tarball. | | npm run example | Run example.ts with ts-node (requires Redis). |


License

MIT.