{"_id":"@boring-stack-pkg/eslint-plugin-bullmq","_rev":"2-bdaf24d1b200eb3cadc68a78d4e8f855","name":"@boring-stack-pkg/eslint-plugin-bullmq","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.1":{"name":"@boring-stack-pkg/eslint-plugin-bullmq","version":"0.1.1","keywords":["eslint","eslintplugin","typescript","bullmq","redis","queue","worker"],"author":"","license":"MIT","_id":"@boring-stack-pkg/eslint-plugin-bullmq@0.1.1","maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"homepage":"https://github.com/AI-Starter-Templates/eslint-plugins#readme","bugs":{"url":"https://github.com/AI-Starter-Templates/eslint-plugins/issues"},"dist":{"shasum":"8731126ab05628f77e6576c710e989657c4db782","tarball":"https://registry.npmjs.org/@boring-stack-pkg/eslint-plugin-bullmq/-/eslint-plugin-bullmq-0.1.1.tgz","fileCount":26,"integrity":"sha512-eWxa9Jx67KgBSAsxZC8XJynS1pf5pcoKH0TSN6I8wLRa8WS34aHdbZjXPa88OlDc9ENA5bbSdxoy+CA/RCltSw==","signatures":[{"sig":"MEYCIQDW52GqShlLW/8AaDxsUS7aL3Hw5Y2Ta4JJVSWn38SbuwIhAKK+LONVFvf8oRK0wdZV+WouFB3M/o5vHourU/ueds3s","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@boring-stack-pkg%2feslint-plugin-bullmq@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":99222},"main":"./dist/index.cjs","type":"module","_from":"file:boring-stack-pkg-eslint-plugin-bullmq-0.1.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":"^20.0.0 || ^22.0.0 || >=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"agjs","email":"hi@aleksandar.xyz"},"_resolved":"/tmp/8dc591a81c696fda8df1a7ae9dd59433/boring-stack-pkg-eslint-plugin-bullmq-0.1.1.tgz","_integrity":"sha512-eWxa9Jx67KgBSAsxZC8XJynS1pf5pcoKH0TSN6I8wLRa8WS34aHdbZjXPa88OlDc9ENA5bbSdxoy+CA/RCltSw==","repository":{"url":"git+https://github.com/AI-Starter-Templates/eslint-plugins.git","type":"git","directory":"eslint-plugin-bullmq"},"_npmVersion":"10.9.7","description":"ESLint plugin enforcing operational-safety rules for BullMQ projects.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","dependencies":{"@typescript-eslint/utils":"8.0.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","eslint":"9.0.0","vitest":"2.0.0","@eslint/js":"9.0.0","typescript":"6.0.3","@types/node":"22.0.0","@typescript-eslint/parser":"8.0.0","@typescript-eslint/rule-tester":"8.0.0"},"peerDependencies":{"eslint":"8.57.0 || ^9.0.0","typescript":">=5.0.0","@typescript-eslint/parser":">=8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/eslint-plugin-bullmq_0.1.1_1779219580186_0.723307885366266","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@boring-stack-pkg/eslint-plugin-bullmq","version":"0.1.3","description":"ESLint plugin enforcing operational-safety rules for BullMQ projects.","type":"module","license":"MIT","author":"","repository":{"type":"git","url":"git+https://github.com/boringstack-xyz/eslint-plugins.git","directory":"eslint-plugin-bullmq"},"publishConfig":{"access":"public"},"sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"keywords":["eslint","eslintplugin","typescript","bullmq","redis","queue","worker"],"peerDependencies":{"@typescript-eslint/parser":">=8.0.0","eslint":"8.57.0 || ^9.0.0","typescript":">=5.0.0"},"dependencies":{"@typescript-eslint/utils":"8.0.0","micromatch":"4.0.5"},"devDependencies":{"@eslint/js":"9.0.0","@types/micromatch":"4.0.9","@types/node":"22.0.0","@typescript-eslint/parser":"8.0.0","@typescript-eslint/rule-tester":"8.0.0","eslint":"9.0.0","tsup":"8.0.0","typescript":"6.0.3","vitest":"2.0.0"},"engines":{"node":"^20.0.0 || ^22.0.0 || >=24.0.0"},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"_id":"@boring-stack-pkg/eslint-plugin-bullmq@0.1.3","bugs":{"url":"https://github.com/boringstack-xyz/eslint-plugins/issues"},"homepage":"https://github.com/boringstack-xyz/eslint-plugins#readme","_integrity":"sha512-XnRrIt00cORNKdkn51HzbhgIamk+GA/MsRXlNAlenARhJUiFM+Qg1Taik6S/sL/jo578UXI3A1+Jg/SjMHfDMg==","_resolved":"/tmp/2266fa5d471300c42c232ad24b9afcf4/boring-stack-pkg-eslint-plugin-bullmq-0.1.3.tgz","_from":"file:boring-stack-pkg-eslint-plugin-bullmq-0.1.3.tgz","_nodeVersion":"22.22.3","_npmVersion":"11.15.0","dist":{"integrity":"sha512-XnRrIt00cORNKdkn51HzbhgIamk+GA/MsRXlNAlenARhJUiFM+Qg1Taik6S/sL/jo578UXI3A1+Jg/SjMHfDMg==","shasum":"af95717c41deea801692d0be3ae765b5548428f5","tarball":"https://registry.npmjs.org/@boring-stack-pkg/eslint-plugin-bullmq/-/eslint-plugin-bullmq-0.1.3.tgz","fileCount":26,"unpackedSize":107337,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC88tJw6zXdq0ZwRd2cNjAAhdVXDqk5CT9zgkBks6k1SwIhAOloUA+p2E0g4RE1SMRrafdoHuyIvcQeaDT/xolUuoKi"}]},"_npmUser":{"name":"agjs","email":"hi@aleksandar.xyz"},"directories":{},"maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eslint-plugin-bullmq_0.1.3_1779695118537_0.37245180184302895"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T19:39:40.052Z","modified":"2026-05-25T07:45:18.803Z","0.1.1":"2026-05-19T19:39:40.339Z","0.1.3":"2026-05-25T07:45:18.683Z"},"bugs":{"url":"https://github.com/boringstack-xyz/eslint-plugins/issues"},"license":"MIT","homepage":"https://github.com/boringstack-xyz/eslint-plugins#readme","keywords":["eslint","eslintplugin","typescript","bullmq","redis","queue","worker"],"repository":{"type":"git","url":"git+https://github.com/boringstack-xyz/eslint-plugins.git","directory":"eslint-plugin-bullmq"},"description":"ESLint plugin enforcing operational-safety rules for BullMQ projects.","maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"readme":"# eslint-plugin-bullmq\n\n[![npm](https://img.shields.io/npm/v/@boring-stack-pkg/eslint-plugin-bullmq?logo=npm)](https://www.npmjs.com/package/@boring-stack-pkg/eslint-plugin-bullmq) [![source](https://img.shields.io/badge/source-github-blue?logo=github)](https://github.com/boringstack-xyz/eslint-plugins/tree/main/eslint-plugin-bullmq)\n\nESLint plugin enforcing operational-safety rules for [BullMQ](https://docs.bullmq.io/) projects.\n\n## Why\n\nBullMQ is fast, durable, and unopinionated — which means most production failure modes live in code patterns that the framework happily accepts. A worker without a close method abandons in-flight jobs on deploy. A worker without a `failed` listener swallows every error silently. A queue without `removeOnComplete` fills Redis. Job retries without `backoff` fire back-to-back. A `concurrency: 0` boots a worker that processes nothing.\n\nThese seven rules pin those patterns down at lint time so they fail in PR review instead of on a 3 a.m. page.\n\n## Install\n\n```sh\npnpm add -D @boring-stack-pkg/eslint-plugin-bullmq @typescript-eslint/parser\n```\n\n## Usage (flat config)\n\n```js\n// eslint.config.mjs\nimport tsParser from \"@typescript-eslint/parser\";\nimport bullmq from \"@boring-stack-pkg/eslint-plugin-bullmq\";\n\nexport default [\n  {\n    files: [\"**/*.{ts,tsx}\"],\n    languageOptions: {\n      parser: tsParser,\n      parserOptions: { ecmaVersion: \"latest\", sourceType: \"module\" },\n    },\n    plugins: { bullmq },\n    rules: bullmq.configs.recommended.rules,\n  },\n];\n```\n\nThe recommended preset enables all seven rules at `\"error\"`.\n\n## Rules\n\n| Rule                                                                                               | Category    | Description                                                                                   |\n| -------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- |\n| [`worker-must-implement-close`](docs/rules/worker-must-implement-close.md)                         | Lifecycle   | Classes that own a `new Worker(...)` must declare `close()` (or alias) for graceful shutdown. |\n| [`worker-must-listen-failed`](docs/rules/worker-must-listen-failed.md)                             | Visibility  | Every Worker must register `.on(\"failed\", ...)` so failures aren't silent.                    |\n| [`job-name-must-be-constant`](docs/rules/job-name-must-be-constant.md)                             | Convention  | `<queue>.add(name, ...)` job names must be identifiers, not inline string literals.           |\n| [`queue-options-must-set-removeoncomplete`](docs/rules/queue-options-must-set-removeoncomplete.md) | Retention   | `removeOnComplete` must be configured per-call or via `defaultJobOptions`.                    |\n| [`queue-options-must-set-removeonfail`](docs/rules/queue-options-must-set-removeonfail.md)         | Retention   | `removeOnFail` must be configured per-call or via `defaultJobOptions`.                        |\n| [`job-options-must-set-attempts`](docs/rules/job-options-must-set-attempts.md)                     | Resilience  | `attempts` must be configured; when `attempts > 1`, `backoff` is also required.               |\n| [`no-blocking-concurrency-zero`](docs/rules/no-blocking-concurrency-zero.md)                       | Correctness | Disallow `new Worker(..., { concurrency: <numericLiteral ≤ 0> })`.                            |\n\n## Examples\n\n### worker-must-implement-close\n\n```ts\n// ❌\nexport class JobService {\n  private worker = new Worker(\"queue\", async () => {});\n}\n\n// ✅\nexport class JobService {\n  private worker = new Worker(\"queue\", async () => {});\n  async close() {\n    await this.worker.close();\n  }\n}\n```\n\n### worker-must-listen-failed\n\n```ts\n// ❌\nconst worker = new Worker(\"queue\", async () => {});\n\n// ✅\nconst worker = new Worker(\"queue\", async () => {});\nworker.on(\"failed\", (job, err) => logger.error({ id: job?.id, err }));\n```\n\n### job-options-must-set-attempts\n\n```ts\n// ❌\nconst emailQueue = new Queue(\"email\");\nemailQueue.add(SEND_EMAIL, { to: \"x\" }, {});\n\n// ✅  (queue-level defaults apply to every add())\nconst emailQueue = new Queue(\"email\", {\n  defaultJobOptions: {\n    removeOnComplete: 1000,\n    removeOnFail: 5000,\n    attempts: 5,\n    backoff: { type: \"exponential\", delay: 1000 },\n  },\n});\n```\n\nFor full per-rule docs and ❌/✅ snippets, see [`docs/rules/`](docs/rules/) and the runnable [`examples/`](examples/).\n\n## Operational rationale\n\nEach rule maps to a real production failure mode:\n\n| Rule                                                       | Failure mode it prevents                                                                                    |\n| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |\n| `worker-must-implement-close`                              | Connection leaks on deploy; jobs in flight are abandoned mid-execution.                                     |\n| `worker-must-listen-failed`                                | Silent task drops — failures don't show up in logs / metrics / alerts.                                      |\n| `job-name-must-be-constant`                                | Drift between producers, workers, and dashboards when a string is renamed in only one place.                |\n| `queue-options-must-set-removeoncomplete` / `removeonfail` | Redis OOM as completed/failed jobs accumulate forever.                                                      |\n| `job-options-must-set-attempts`                            | No retries on transient failures; or retries that fire so fast they exhaust the budget on identical errors. |\n| `no-blocking-concurrency-zero`                             | Worker boots, listens, processes nothing. Often the symptom of a missing config default.                    |\n\n## Limitations of static analysis\n\n- **Cross-file queue/worker tracking is out of scope.** A queue defined in one module and used in another can't have its `defaultJobOptions` consulted from the call site.\n- **Queue identification falls back to the `Queue$` name suffix.** A non-BullMQ object whose variable name happens to end in `Queue` will be treated as one. Tighten via `queueNamePattern` if needed.\n- **Listeners attached via helpers** (e.g., `attachStandardListeners(worker)`) are invisible to the rule. Subscribe inline.\n- **`worker-must-implement-close`** only checks classes — module-level `new Worker(...)` instances need their cleanup wired into `process.on(\"SIGTERM\")` directly (see `examples/valid/standalone-worker.ts`).\n\n## Development\n\n```sh\npnpm install\npnpm test\npnpm typecheck\npnpm build\n```\n\n## Release\n\nTag `v*` locally and push the tag — `.github/workflows/release.yml` runs `pnpm publish --access public` with `NPM_TOKEN`.\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}