@abdoseadaa/smart-interval
v1.0.1
Published
Production-grade setInterval replacement backed by BullMQ and Redis. No overlapping runs, persistence, retries, and graceful shutdown.
Maintainers
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.triggerso you know where it was invoked
Requirements
- Node.js ≥ 18
- Redis (any version compatible with BullMQ / ioredis)
Installation
npm i @abdoseadaa/smart-intervalPeer / 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 afterlast_run_at; must be in[0, every]. - before:
{ when: number; callback: SmartIntervalCallback }—when= ms beforenext_run_at; must be in[0, every]. - middle:
{ callback: SmartIntervalCallback }— runs atlast_run_at + every/2(nowhen).
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:
voidorPromise<void>. Usectx.triggerto 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.
