{"_id":"@abdoseadaa/smart-interval","_rev":"2-45b1a4622b3779060ce17e4d61973cfb","name":"@abdoseadaa/smart-interval","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@abdoseadaa/smart-interval","version":"1.0.0","keywords":["interval","setInterval","cron","scheduler","bullmq","redis","job-queue","recurring","background-jobs"],"author":"","license":"MIT","_id":"@abdoseadaa/smart-interval@1.0.0","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"homepage":"https://gitlab.com/abdom.seada/smart-interval#readme","bugs":{"url":"https://gitlab.com/abdom.seada/smart-interval/issues"},"dist":{"shasum":"0348bd4b98744257fe416193409a0586cb77985b","tarball":"https://registry.npmjs.org/@abdoseadaa/smart-interval/-/smart-interval-1.0.0.tgz","fileCount":27,"integrity":"sha512-sif9qn3NDEn14Ku/FD6beLHLNp5f6Qn0LydUkKR2wFeZoRrvSOd8P18MG7lEr9y698sCAhEq1KQ4GbSTvwoCRg==","signatures":[{"sig":"MEQCIA5bb8njJVAY3LC86HHugTLUXqslVVDfsBZvLFyiyjHBAiBRRqVRtNQHwonuy2FWgFlWw998QZhO7co2CYZvOxSfXg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46131},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"gitHead":"19469c331b3eb0f3494549deb8c537876da52e36","scripts":{"pkg":"npm run build && npm pack --dry-run","build":"npm run clean && tsc -p tsconfig.build.json","clean":"node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"","example":"npm run build && node example.js","example:ts":"ts-node -p tsconfig.json example.ts","prepublishOnly":"npm run build"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"repository":{"url":"git+https://gitlab.com/abdom.seada/smart-interval.git","type":"git"},"_npmVersion":"11.10.1","description":"Production-grade setInterval replacement backed by BullMQ and Redis. No overlapping runs, persistence, retries, and graceful shutdown.","directories":{},"_nodeVersion":"22.20.0","dependencies":{"bullmq":"^5.0.0","ioredis":"^5.3.0"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.0","typescript":"^5.0.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/smart-interval_1.0.0_1772026509708_0.7386810699157926","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@abdoseadaa/smart-interval","version":"1.0.1","description":"Production-grade setInterval replacement backed by BullMQ and Redis. No overlapping runs, persistence, retries, and graceful shutdown.","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"clean":"node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"","build":"npm run clean && tsc -p tsconfig.build.json","prepublishOnly":"npm run build","pkg":"npm run build && npm pack --dry-run","example":"npm run build && node example.js","example:ts":"ts-node -p tsconfig.json example.ts"},"keywords":["interval","setInterval","cron","scheduler","bullmq","redis","job-queue","recurring","background-jobs"],"author":"","license":"MIT","repository":{"type":"git","url":"git+https://gitlab.com/abdom.seada/smart-interval.git"},"engines":{"node":">=18"},"dependencies":{"bullmq":"^5.0.0","ioredis":"^5.3.0"},"devDependencies":{"@types/node":"^20.0.0","typescript":"^5.0.0","ts-node":"^10.9.0"},"gitHead":"19469c331b3eb0f3494549deb8c537876da52e36","_id":"@abdoseadaa/smart-interval@1.0.1","bugs":{"url":"https://gitlab.com/abdom.seada/smart-interval/issues"},"homepage":"https://gitlab.com/abdom.seada/smart-interval#readme","_nodeVersion":"22.20.0","_npmVersion":"11.10.1","dist":{"integrity":"sha512-1XjACRekdyPrNPnGon/A4YAZIp4rdsSToGxYWWzff/pRVy6b1/MeBkTbeIN/1nuMIyX0fTnRoh83AOrrJhSV7g==","shasum":"85e10116effb6be0b45a24ed0cee7dbc0dc727ca","tarball":"https://registry.npmjs.org/@abdoseadaa/smart-interval/-/smart-interval-1.0.1.tgz","fileCount":27,"unpackedSize":46149,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCs9wP5+BVkUnwDtPRr9PUD6zPbXQJ+oQpFlG8mpDT8kwIgCsPw2a4SO0BU0YJ/pgJNPnoM+RCTygJSvmMTuf3tvTQ="}]},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"directories":{},"maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/smart-interval_1.0.1_1772027068668_0.44755027419303617"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-25T13:35:09.619Z","modified":"2026-02-25T13:44:28.959Z","1.0.0":"2026-02-25T13:35:09.866Z","1.0.1":"2026-02-25T13:44:28.836Z"},"bugs":{"url":"https://gitlab.com/abdom.seada/smart-interval/issues"},"license":"MIT","homepage":"https://gitlab.com/abdom.seada/smart-interval#readme","keywords":["interval","setInterval","cron","scheduler","bullmq","redis","job-queue","recurring","background-jobs"],"repository":{"type":"git","url":"git+https://gitlab.com/abdom.seada/smart-interval.git"},"description":"Production-grade setInterval replacement backed by BullMQ and Redis. No overlapping runs, persistence, retries, and graceful shutdown.","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"readme":"# smart-interval\r\n\r\n**Production-grade `setInterval` replacement** backed by [BullMQ](https://docs.bullmq.io/) and Redis. Use it when you need recurring work that must not overlap, survive restarts, and shut down cleanly.\r\n\r\n- **No overlapping runs** — one job at a time per interval (concurrency 1)\r\n- **Redis-backed** — schedule and job state persist across process restarts\r\n- **Retries & backoff** — configurable attempts and delay on failure\r\n- **Graceful shutdown** — stop worker and queue without leaving orphan jobs\r\n- **TypeScript** — full typings and run context (`every`, `last_run_at`, `next_run_at`, `trigger`)\r\n- **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\r\n\r\n---\r\n\r\n## Requirements\r\n\r\n- **Node.js** ≥ 18\r\n- **Redis** (any version compatible with BullMQ / ioredis)\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm i @abdoseadaa/smart-interval\r\n```\r\n\r\n**Peer / runtime:** BullMQ and ioredis are listed as dependencies and will be installed with the package.\r\n\r\n---\r\n\r\n## Quick start\r\n\r\n```ts\r\nimport { configureRedis, setSmartInterval } from '@abdoseadaa/smart-interval';\r\n\r\n// 1. Configure Redis once at app startup\r\nconfigureRedis({\r\n  host: process.env.REDIS_HOST ?? 'localhost',\r\n  port: Number(process.env.REDIS_PORT ?? 6379),\r\n  password: process.env.REDIS_PASSWORD,\r\n});\r\n\r\n// 2. Start a recurring task\r\nconst handle = await setSmartInterval({\r\n  name: 'sync-orders',\r\n  every: 60_000, // ms\r\n  callback: async (ctx) => {\r\n    console.log('Running at', ctx.last_run_at, 'next at', ctx.next_run_at);\r\n    await doWork();\r\n    if (done) await ctx.stop();\r\n  },\r\n});\r\n\r\n// 3. Graceful shutdown\r\nprocess.on('SIGTERM', async () => {\r\n  await handle.stop();\r\n  process.exit(0);\r\n});\r\n```\r\n\r\n---\r\n\r\n## API reference\r\n\r\n### `configureRedis(config)`\r\n\r\nConfigures the Redis connection used by **all** SmartInterval queues and workers. Call this **once** before any `setSmartInterval`.\r\n\r\n| Parameter | Type | Required | Description |\r\n|-----------|------|----------|-------------|\r\n| `config` | `RedisConfig` | Yes | Redis connection options. |\r\n\r\n**Returns:** `void`\r\n\r\n**Throws:** Nothing. If Redis is not configured and you call `setSmartInterval`, the library will throw at that time.\r\n\r\n---\r\n\r\n### `setSmartInterval(options)`\r\n\r\nCreates 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.\r\n\r\n| Parameter | Type | Required | Description |\r\n|-----------|------|----------|-------------|\r\n| `options` | `SmartIntervalOptions` | Yes | Interval name, period, callback, and optional retry/hooks. |\r\n\r\n**Returns:** `Promise<SmartIntervalHandle>`\r\n\r\n**Throws:** If Redis was not configured, or on Redis/queue errors.\r\n\r\n---\r\n\r\n## Types\r\n\r\n### `RedisConfig`\r\n\r\nOptions passed to `configureRedis()`.\r\n\r\n| Property | Type | Required | Description |\r\n|----------|------|----------|-------------|\r\n| `host` | `string` | Yes | Redis host (e.g. `'localhost'`). |\r\n| `port` | `number` | Yes | Redis port (e.g. `6379`). |\r\n| `password` | `string` | No | Redis password. |\r\n| `db` | `number` | No | Redis DB index. Default: `0`. |\r\n| `tls` | `boolean` | No | Use TLS. Default: `false`. |\r\n\r\n---\r\n\r\n### `SmartIntervalOptions`\r\n\r\nOptions passed to `setSmartInterval()`.\r\n\r\n| Property | Type | Required | Description |\r\n|----------|------|----------|-------------|\r\n| `name` | `string` | Yes | Unique name for this interval (used as BullMQ queue name). |\r\n| `every` | `number` | Yes | Interval in **milliseconds** (e.g. `60_000` = 1 minute). |\r\n| `callback` | `SmartIntervalCallback` | Yes | Function run on each tick; receives `SmartIntervalContext`. |\r\n| `retries` | `number` | No | Number of retries on failure (default: `3`). Total attempts = retries + 1. |\r\n| `retryDelay` | `number` | No | Delay in ms between retries (default: `5000`). |\r\n| `keepCompleted` | `number` | No | Max completed jobs to keep in Redis (default: `50`). |\r\n| `keepFailed` | `number` | No | Max failed jobs to keep in Redis (default: `20`). |\r\n| `onError` | `(error: Error, jobId?: string) => void` | No | Called when a job fails after all retries. |\r\n| `onComplete` | `(jobId?: string) => void` | No | Called when a job completes successfully. |\r\n| `after` | `SmartIntervalAfterHook` | No | Run within the cycle: **after** `last_run_at` by `when` ms. `when` must be in `[0, every]`. |\r\n| `before` | `SmartIntervalBeforeHook` | No | Run within the cycle: **before** `next_run_at` by `when` ms. `when` must be in `[0, every]`. |\r\n| `middle` | `SmartIntervalMiddleHook` | No | Run at the **middle** of the cycle (`last_run_at + every/2`). No `when` — fixed. |\r\n\r\n---\r\n\r\n### `SmartIntervalTrigger`\r\n\r\nIdentifies which callback was invoked. Every callback (main or hook) receives the same `SmartIntervalContext` with a `trigger` field:\r\n\r\n| Value | Meaning |\r\n|--------|--------|\r\n| `'interval'` | Main interval tick (your `callback`). |\r\n| `'after'` | Hook run after `last_run_at` by `after.when` ms. |\r\n| `'before'` | Hook run before `next_run_at` by `before.when` ms. |\r\n| `'middle'` | Hook run at the middle of the cycle. |\r\n\r\n```ts\r\ntype SmartIntervalTrigger = 'interval' | 'after' | 'before' | 'middle';\r\n```\r\n\r\n---\r\n\r\n### `SmartIntervalAfterHook` / `SmartIntervalBeforeHook` / `SmartIntervalMiddleHook`\r\n\r\n- **after:** `{ when: number; callback: SmartIntervalCallback }` — `when` = ms after `last_run_at`; must be in `[0, every]`.\r\n- **before:** `{ when: number; callback: SmartIntervalCallback }` — `when` = ms before `next_run_at`; must be in `[0, every]`.\r\n- **middle:** `{ callback: SmartIntervalCallback }` — runs at `last_run_at + every/2` (no `when`).\r\n\r\nIf `when` is outside `[0, every]`, `setSmartInterval` throws at startup.\r\n\r\n---\r\n\r\n### `SmartIntervalCallback`\r\n\r\nType of the `callback` in `SmartIntervalOptions`:\r\n\r\n```ts\r\ntype SmartIntervalCallback = (ctx: SmartIntervalContext) => Promise<void> | void;\r\n```\r\n\r\n- **Parameter:** `ctx` — run context (handle + `every`, `last_run_at`, `next_run_at`, `trigger`).\r\n- **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.\r\n\r\n---\r\n\r\n### `SmartIntervalContext`\r\n\r\nObject 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.\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `trigger` | `SmartIntervalTrigger` | Which callback was invoked: `'interval'`, `'after'`, `'before'`, or `'middle'`. |\r\n| `every` | `number` | Interval in ms (same as `options.every`). |\r\n| `last_run_at` | `Date \\| null` | When this run started (from job metadata), or `null` if unknown. |\r\n| `next_run_at` | `Date \\| null` | Approximate next scheduled run time (current run start + `every`), or `null`. |\r\n| `stop` | `() => Promise<void>` | Stop the interval and close the worker/queue. |\r\n| `pause` | `() => Promise<void>` | Pause the repeatable schedule (jobs stay in queue). |\r\n| `resume` | `() => Promise<void>` | Resume a paused interval. |\r\n| `runNow` | `() => Promise<void>` | Enqueue one immediate run (in addition to the schedule). |\r\n| `getStatus` | `() => Promise<SmartIntervalStatus>` | Get current queue/handle status. |\r\n\r\n---\r\n\r\n### `SmartIntervalHandle`\r\n\r\nReturn type of `setSmartInterval()`. Same control methods as `SmartIntervalContext` (no `every` / `last_run_at` / `next_run_at`).\r\n\r\n| Method | Signature | Description |\r\n|--------|------------|-------------|\r\n| `stop` | `() => Promise<void>` | Stop the interval, close worker, obliterate and close queue. |\r\n| `pause` | `() => Promise<void>` | Pause the repeatable schedule. |\r\n| `resume` | `() => Promise<void>` | Resume after pause. |\r\n| `runNow` | `() => Promise<void>` | Trigger one extra run now. |\r\n| `getStatus` | `() => Promise<SmartIntervalStatus>` | Get current status. |\r\n\r\n---\r\n\r\n### `SmartIntervalStatus`\r\n\r\nReturn type of `handle.getStatus()` / `ctx.getStatus()`.\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `name` | `string` | Interval name (queue name). |\r\n| `isPaused` | `boolean` | Whether the queue is paused. |\r\n| `waitingCount` | `number` | Jobs waiting to be processed. |\r\n| `activeCount` | `number` | Jobs currently processing. |\r\n| `completedCount` | `number` | Completed jobs (up to `keepCompleted`). |\r\n| `failedCount` | `number` | Failed jobs (up to `keepFailed`). |\r\n\r\n---\r\n\r\n## Examples\r\n\r\n### Basic recurring task\r\n\r\n```ts\r\nconst handle = await setSmartInterval({\r\n  name: 'health-check',\r\n  every: 30_000,\r\n  callback: async (ctx) => {\r\n    await pingServices();\r\n    console.log('Next run at', ctx.next_run_at);\r\n  },\r\n});\r\n```\r\n\r\n### Use run metadata and stop from inside\r\n\r\n```ts\r\nawait setSmartInterval({\r\n  name: 'sync-orders',\r\n  every: 60_000,\r\n  callback: async (ctx) => {\r\n    console.log('Interval (ms):', ctx.every);\r\n    console.log('This run started:', ctx.last_run_at);\r\n    const synced = await syncOrders();\r\n    if (synced >= 0) await ctx.stop(); // stop when no more work\r\n  },\r\n});\r\n```\r\n\r\n### Error and completion hooks\r\n\r\n```ts\r\nawait setSmartInterval({\r\n  name: 'report-generation',\r\n  every: 3600_000,\r\n  retries: 5,\r\n  retryDelay: 10_000,\r\n  callback: async (ctx) => await generateReport(),\r\n  onError: (err, jobId) => {\r\n    logger.error('Report job failed', { jobId, error: err.message });\r\n  },\r\n  onComplete: (jobId) => {\r\n    logger.info('Report job completed', { jobId });\r\n  },\r\n});\r\n```\r\n\r\n### Cycle hooks (after, before, middle)\r\n\r\nRun 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.\r\n\r\n```ts\r\nawait setSmartInterval({\r\n  name: 'sync-with-hooks',\r\n  every: 60_000,\r\n  callback: async (ctx) => {\r\n    if (ctx.trigger !== 'interval') return;\r\n    await doMainWork();\r\n  },\r\n  after: {\r\n    when: 5_000, // 5s after last_run_at\r\n    callback: async (ctx) => {\r\n      console.log('After hook at', ctx.last_run_at, 'trigger:', ctx.trigger); // trigger === 'after'\r\n    },\r\n  },\r\n  before: {\r\n    when: 10_000, // 10s before next_run_at\r\n    callback: async (ctx) => {\r\n      console.log('Before hook, next at', ctx.next_run_at, 'trigger:', ctx.trigger); // 'before'\r\n    },\r\n  },\r\n  middle: {\r\n    callback: async (ctx) => {\r\n      console.log('Middle of cycle', ctx.trigger); // 'middle'\r\n    },\r\n  },\r\n});\r\n```\r\n\r\n`after.when` and `before.when` must be in `[0, every]`; otherwise `setSmartInterval` throws.\r\n\r\n---\r\n\r\n### Pause / resume and run now\r\n\r\n```ts\r\nconst handle = await setSmartInterval({\r\n  name: 'notifications',\r\n  every: 5_000,\r\n  callback: async () => await sendBatch(),\r\n});\r\n\r\nawait handle.pause();   // stop scheduled runs\r\n// ... later ...\r\nawait handle.resume(); // resume schedule\r\nawait handle.runNow(); // one extra run immediately\r\nconst status = await handle.getStatus();\r\nconsole.log(status.waitingCount, status.activeCount);\r\n```\r\n\r\n### Graceful shutdown\r\n\r\n```ts\r\nconst intervals = [\r\n  await setSmartInterval({ name: 'job-a', every: 1000, callback: jobA }),\r\n  await setSmartInterval({ name: 'job-b', every: 2000, callback: jobB }),\r\n];\r\n\r\nprocess.on('SIGTERM', async () => {\r\n  await Promise.all(intervals.map((h) => h.stop()));\r\n  process.exit(0);\r\n});\r\n```\r\n\r\n---\r\n\r\n## Scripts (development)\r\n\r\n| Script | Description |\r\n|--------|-------------|\r\n| `npm run build` | Clean `dist/` and compile TypeScript (emits JS + `.d.ts`). |\r\n| `npm run pkg` | Build and run `npm pack --dry-run` to inspect the publish tarball. |\r\n| `npm run example` | Run `example.ts` with ts-node (requires Redis). |\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT.\r\n","readmeFilename":"README.md"}