{"_id":"@codylabs/nestjs-redis-schedule","_rev":"4-68fc00306d30767336ecdd289775ff3d","name":"@codylabs/nestjs-redis-schedule","dist-tags":{"latest":"0.2.0"},"versions":{"0.0.1":{"name":"@codylabs/nestjs-redis-schedule","version":"0.0.1","author":"Cody Nguyen","license":"MIT","_id":"@codylabs/nestjs-redis-schedule@0.0.1","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"dist":{"shasum":"d2d3bb8482ff97c7d960f9eb953b1757a866acbd","tarball":"https://registry.npmjs.org/@codylabs/nestjs-redis-schedule/-/nestjs-redis-schedule-0.0.1.tgz","fileCount":75,"integrity":"sha512-vFWcrdZhNJ7twDKJ4Q9xCy5tkFJ5kNI4EOdqUDs+qow7XnyBITs+3OW+LQGW7ta0TvE6iJAtoCTzQZO4/IJCng==","signatures":[{"sig":"MEYCIQDff0p+pMuaWBu0S1qGTaffOPr0KttEu/Xv1FMl8U0sLAIhAOHcQDxnS28vQOhCKej+HlWPJcv5i+ElVsqw6ctce0/k","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":385808},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"test":"jest --passWithNoTests","build":"tsc -p tsconfig.build.json","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test:watch":"jest --watch"},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"description":"Drop-in replacement for @nestjs/schedule with Redis-backed distributed cron execution","directories":{},"_nodeVersion":"24.16.0","dependencies":{"croner":"^10.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@nestjs/testing":"^11.1.24","@codylabs/typescript-configs":"0.0.8"},"peerDependencies":{"redis":"^5.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-redis-schedule_0.0.1_1779866802083_0.2980995799816062","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@codylabs/nestjs-redis-schedule","version":"0.0.2","author":"Cody Nguyen","license":"MIT","_id":"@codylabs/nestjs-redis-schedule@0.0.2","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"homepage":"https://github.com/cuongnd1705/codylabs#readme","bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"dist":{"shasum":"e34556b404e5df9e3b6a14f5a96ddd527777ce4d","tarball":"https://registry.npmjs.org/@codylabs/nestjs-redis-schedule/-/nestjs-redis-schedule-0.0.2.tgz","fileCount":75,"integrity":"sha512-KkoPhlycc9PiSFIlUjE39slCL5/ra9WRkqyf2P1PmasahVlUIs7g+PjW7+yZ3nEgtQ1YLfOfWcdNWtlECGZhAQ==","signatures":[{"sig":"MEQCIDiZpoI9R9w3NHDS5lDdS0oUNoOGrI+Yl2yxQUncgE4MAiACYIBxDDXiH5gwjO/FW7FSBygLzjH5KWFQwI0IbSp9hQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":386030},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"test":"jest --passWithNoTests","build":"tsc -p tsconfig.build.json","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test:watch":"jest --watch"},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"repository":{"url":"git+https://github.com/cuongnd1705/codylabs.git","type":"git"},"description":"Drop-in replacement for @nestjs/schedule with Redis-backed distributed cron execution","directories":{},"_nodeVersion":"24.16.0","dependencies":{"croner":"^10.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@nestjs/testing":"^11.1.24","@codylabs/typescript-configs":"0.0.9"},"peerDependencies":{"redis":"^6.0.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-redis-schedule_0.0.2_1780126504296_0.4722327075587549","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@codylabs/nestjs-redis-schedule","version":"0.1.0","author":"Cody Nguyen","license":"MIT","_id":"@codylabs/nestjs-redis-schedule@0.1.0","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"homepage":"https://github.com/cuongnd1705/codylabs#readme","bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"dist":{"shasum":"af2cdfe24afd2220967b8bf6de54a9146e013147","tarball":"https://registry.npmjs.org/@codylabs/nestjs-redis-schedule/-/nestjs-redis-schedule-0.1.0.tgz","fileCount":75,"integrity":"sha512-r3D579vLwHnKmFKZyc9O6V5/9p9/chfcMURVmXfBHUq1VPcXpQjWaY8oDyNKGJP0mt/FGx8RA5D+IDwpI2vRzA==","signatures":[{"sig":"MEYCIQCzKl7qSwDPexffRBL06L4PV8na36WJYAYSCFJVlyfUmwIhAL7pwRpw0t8UW+Pn8jNBBVLKku0WkqbSFO+tcQyE9LdP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":402082},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"test":"jest --passWithNoTests","build":"tsc -p tsconfig.build.json","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test:watch":"jest --watch"},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"repository":{"url":"git+https://github.com/cuongnd1705/codylabs.git","type":"git"},"description":"Drop-in replacement for @nestjs/schedule with Redis-backed distributed cron execution","directories":{},"_nodeVersion":"24.18.0","dependencies":{"croner":"^10.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@nestjs/testing":"^11.1.28","@codylabs/typescript-configs":"0.0.9"},"peerDependencies":{"redis":"^6.0.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-redis-schedule_0.1.0_1784477196476_0.40991620617039537","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@codylabs/nestjs-redis-schedule","version":"0.2.0","description":"Drop-in replacement for @nestjs/schedule with Redis-backed distributed cron execution","homepage":"https://github.com/cuongnd1705/codylabs#readme","bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"license":"MIT","author":"Cody Nguyen","repository":{"type":"git","url":"git+https://github.com/cuongnd1705/codylabs.git"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"publishConfig":{"access":"public"},"dependencies":{"croner":"^10.0.1"},"devDependencies":{"@codylabs/typescript-configs":"0.1.0","@nestjs/testing":"^12.0.1","@swc/core":"^1.16.1","unplugin-swc":"^1.5.11","vitest":"^4.1.11"},"peerDependencies":{"@nestjs/common":"^12.0.0","@nestjs/core":"^12.0.0","redis":"^6.0.0"},"scripts":{"build":"tsc -p tsconfig.build.json","clean":"rm -rf .turbo node_modules dist","prebuild":"rimraf dist","test":"vitest run --passWithNoTests","test:watch":"vitest"},"_nodeVersion":"24.20.0","_id":"@codylabs/nestjs-redis-schedule@0.2.0","dist":{"integrity":"sha512-fwsNR4G7ZnQtgdzYVFo805dr7n9MUNbExL3rN279HiWoDp1/z+kklbvEpvhMxOYzg7Yk7SzR+MG2zRHkggXs/w==","shasum":"84e8650c77afa610ff7e0369d3b8d00ab193faaf","tarball":"https://registry.npmjs.org/@codylabs/nestjs-redis-schedule/-/nestjs-redis-schedule-0.2.0.tgz","fileCount":75,"unpackedSize":362409,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBzBy92usIdCIQO9bsLwD7ZSALLCi8L73oz9rlZ76MbnAiEA3q1X4zXw8L5cprzMNXgpPxaVXIcRAgeRwS860FWsHY4="}]},"_npmUser":{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"},"directories":{},"maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-redis-schedule_0.2.0_1788017131423_0.9298140712630589"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-27T07:26:41.949Z","modified":"2026-08-29T15:25:31.831Z","0.0.1":"2026-05-27T07:26:42.238Z","0.0.2":"2026-05-30T07:35:04.473Z","0.1.0":"2026-07-19T16:06:36.628Z","0.2.0":"2026-08-29T15:25:31.612Z"},"bugs":{"url":"https://github.com/cuongnd1705/codylabs/issues"},"author":"Cody Nguyen","license":"MIT","homepage":"https://github.com/cuongnd1705/codylabs#readme","repository":{"type":"git","url":"git+https://github.com/cuongnd1705/codylabs.git"},"description":"Drop-in replacement for @nestjs/schedule with Redis-backed distributed cron execution","maintainers":[{"name":"cuongnd1705","email":"cuongnd.work@gmail.com"}],"readme":"# @codylabs/nestjs-redis-schedule\n\nDrop-in replacement for `@nestjs/schedule` with Redis-backed distributed cron execution.\n\n## Features\n\n- **Drop-in replacement** — same `@Cron`, `@Interval`, `@Timeout` decorators and `SchedulerRegistry` API as `@nestjs/schedule`\n- **Distributed cron execution** — Redis ZSET + atomic Lua script guarantees exactly one instance fires per tick\n- **Persistence across restarts** — next-run timestamps are stored in Redis; missed jobs caught on startup\n- **Missed-execution handling** — configurable threshold distinguishes catchup executions from truly-stale skips\n- **Optional at-least-once execution** — renewable leases recover work after handler or process failures\n- **Works with any `redis` v6 client** — `RedisClientType`, `RedisClusterType`, `RedisSentinelType`\n\n## Installation\n\n```sh\n# npm\nnpm install @codylabs/nestjs-redis-schedule redis\n\n# pnpm\npnpm add @codylabs/nestjs-redis-schedule redis\n```\n\n## Setup\n\n### Synchronous\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { createClient } from 'redis';\nimport { ScheduleModule } from '@codylabs/nestjs-redis-schedule';\n\nconst redisClient = createClient({ url: 'redis://localhost:6379' });\nawait redisClient.connect();\n\n@Module({\n  imports: [ScheduleModule.forRoot({ client: redisClient })],\n})\nexport class AppModule {}\n```\n\n### Asynchronous (recommended)\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { RedisModule, RedisToken } from '@codylabs/nestjs-redis';\nimport { ScheduleModule } from '@codylabs/nestjs-redis-schedule';\n\n@Module({\n  imports: [\n    RedisModule.forRoot({ options: { url: 'redis://localhost:6379' } }),\n    ScheduleModule.forRootAsync({\n      inject: [RedisToken()],\n      useFactory: (client) => ({ client }),\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n## Module options\n\n| Option            | Type                                                       | Default          | Description                                                 |\n| ----------------- | ---------------------------------------------------------- | ---------------- | ----------------------------------------------------------- |\n| `client`          | `RedisClientType \\| RedisClusterType \\| RedisSentinelType` | **required**     | Connected Redis client                                      |\n| `keyPrefix`       | `string`                                                   | `'scheduler'`    | Prefix for all Redis keys created by this module            |\n| `shutdownTimeout` | `number`                                                   | `5000`           | Max ms to wait for in-flight handlers to finish on shutdown |\n| `executionMode`   | `'at-most-once' \\| 'at-least-once'`                        | `'at-most-once'` | Execution delivery guarantee                                |\n| `leaseDuration`   | `number`                                                   | `30000`          | Lease duration in ms; renewed while a handler is running    |\n| `maxRetries`      | `number`                                                   | `3`              | Retries after a failed or interrupted leased execution      |\n| `cronJobs`        | `boolean`                                                  | `true`           | Enable `@Cron` discovery                                    |\n| `intervals`       | `boolean`                                                  | `true`           | Enable `@Interval` discovery                                |\n| `timeouts`        | `boolean`                                                  | `true`           | Enable `@Timeout` discovery                                 |\n\n### Reliable execution\n\nThe default `at-most-once` mode preserves the original behavior: a due occurrence is removed before its handler\nruns, so it is not duplicated but can be lost if the process exits at that point. Enable leased execution when a\nmissed occurrence is less acceptable than a possible duplicate:\n\n```typescript\nScheduleModule.forRootAsync({\n  inject: [RedisToken()],\n  useFactory: (client) => ({\n    client,\n    executionMode: 'at-least-once',\n    leaseDuration: 30_000,\n    maxRetries: 3,\n  }),\n});\n```\n\nLeases are renewed while handlers run. A failed handler is retried after its lease expires, and an occurrence\nclaimed by a process that crashes is recovered by another instance. Handlers used with `at-least-once` mode must\nbe idempotent because a crash between completing the side effect and acknowledging the lease can cause a retry.\n\nRedis keys use a shared `{schedule}` hash tag so all atomic scripts also work with Redis Cluster. Upgrading from a\nversion that used `<prefix>:jobs` keys creates a fresh schedule under the new key layout.\n\n## Decorators\n\n### `@Cron(expression, options?)`\n\nSchedules a method as a distributed cron job. Only one instance in the cluster will execute the handler per tick.\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { Cron, CronExpression } from '@codylabs/nestjs-redis-schedule';\n\n@Injectable()\nexport class TasksService {\n  @Cron(CronExpression.EVERY_MINUTE)\n  handleCron() {\n    // runs on exactly one instance per tick\n  }\n\n  @Cron('0 9 * * MON-FRI', { name: 'weekday-report', timeZone: 'America/New_York' })\n  handleWeekdayReport() {}\n\n  @Cron('0 0 * * *', { utcOffset: 330 }) // UTC+5:30\n  handleMidnightIST() {}\n}\n```\n\n**`@Cron` options**\n\n| Option      | Type      | Default         | Description                                                                             |\n| ----------- | --------- | --------------- | --------------------------------------------------------------------------------------- |\n| `name`      | `string`  | cron expression | Unique job name; used as the key in `SchedulerRegistry`                                 |\n| `timeZone`  | `string`  | —               | IANA timezone name (e.g. `'America/New_York'`). Mutually exclusive with `utcOffset`     |\n| `utcOffset` | `number`  | —               | UTC offset in **minutes** (e.g. `330` for UTC+5:30). Mutually exclusive with `timeZone` |\n| `disabled`  | `boolean` | `false`         | Skip registration entirely                                                              |\n| `threshold` | `number`  | `250`           | Ms of execution delay before a missed tick is skipped instead of caught up              |\n\n### `@Interval(timeout)` / `@Interval(name, timeout)`\n\nSchedules a method with `setInterval`. Runs on **every** instance — not distributed.\n\n```typescript\n@Interval('health-check', 30_000)\ncheckHealth() {}\n```\n\n### `@Timeout(timeout)` / `@Timeout(name, timeout)`\n\nSchedules a method with `setTimeout`. Runs on **every** instance — not distributed.\n\n```typescript\n@Timeout('startup-task', 5_000)\nonStartup() {}\n```\n\n## SchedulerRegistry\n\nInject `SchedulerRegistry` to manage jobs at runtime.\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { SchedulerRegistry } from '@codylabs/nestjs-redis-schedule';\n\n@Injectable()\nexport class AdminService {\n  constructor(private readonly schedulerRegistry: SchedulerRegistry) {}\n\n  async pauseJob(name: string) {\n    const job = this.schedulerRegistry.getCronJob(name);\n    await job.stop();\n  }\n\n  async resumeJob(name: string) {\n    const job = this.schedulerRegistry.getCronJob(name);\n    await job.start();\n  }\n\n  listJobs() {\n    return [...this.schedulerRegistry.getCronJobs().entries()].map(([name, job]) => ({\n      name,\n      expression: job.expression,\n      nextRun: new Date(job.nextTs),\n    }));\n  }\n}\n```\n\n**`CronJobHandle` interface**\n\n| Member       | Description                                     |\n| ------------ | ----------------------------------------------- |\n| `name`       | Job name                                        |\n| `expression` | Cron expression string                          |\n| `nextTs`     | Unix timestamp (ms) of the next scheduled run   |\n| `start()`    | Re-registers the job in Redis and the poll loop |\n| `stop()`     | Removes the job from Redis and the poll loop    |\n\n**`SchedulerRegistry` methods**\n\n| Method                  | Description                                                        |\n| ----------------------- | ------------------------------------------------------------------ |\n| `getCronJob(name)`      | Returns the `CronJobHandle` for a cron job (throws if not found)   |\n| `getCronJobs()`         | Returns a `Map<string, CronJobHandle>` of all registered cron jobs |\n| `doesExist(type, name)` | Checks whether a `'cron'`, `'interval'`, or `'timeout'` job exists |\n| `deleteCronJob(name)`   | Stops the job and removes it from the registry                     |\n| `addCronJob(name, job)` | Registers an externally-created `CronJobHandle`                    |\n\n## Migrating from `@nestjs/schedule`\n\n1. Swap the package:\n\n```diff\n-import { ScheduleModule, Cron, CronExpression } from '@nestjs/schedule';\n+import { ScheduleModule, Cron, CronExpression } from '@codylabs/nestjs-redis-schedule';\n```\n\n2. Pass a Redis client when importing the module:\n\n```diff\n-ScheduleModule.forRoot()\n+ScheduleModule.forRootAsync({\n+  inject: [RedisToken()],\n+  useFactory: (client) => ({ client }),\n+})\n```\n\nEverything else (`@Cron`, `@Interval`, `@Timeout`, `SchedulerRegistry`, `CronExpression`) is API-compatible.\n\n> **Note:** `@Interval` and `@Timeout` use native Node.js timers and run on every instance, identical to `@nestjs/schedule`. Only `@Cron` jobs are distributed via Redis.\n\n## License\n\nMIT\n","readmeFilename":""}