{"_id":"@anishhs/retryq","_rev":"3-6dfe9d431215e06f6423ef0eebd195dc","name":"@anishhs/retryq","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@anishhs/retryq","version":"1.0.0","keywords":["retryq","retry","queue","manager","priority","jobs","asynchronous","concurrent","typescript","anishhs"],"author":{"name":"Anish Shekh"},"license":"ISC","_id":"@anishhs/retryq@1.0.0","maintainers":[{"name":"arc22-dev","email":"anishsh701@gmail.com"}],"dist":{"shasum":"612a53f74f951256c7239f1f25e4432457b9cad9","tarball":"https://registry.npmjs.org/@anishhs/retryq/-/retryq-1.0.0.tgz","fileCount":5,"integrity":"sha512-OZsX1luJAyXVuQgnF/CFG/VmiOfkP+zzgIqyuEH2gJXX/G3k8igqoHhiMtWgCo45VQe/o3AYBaLMcyhP9nvudQ==","signatures":[{"sig":"MEYCIQC1t0xRn36lWDpLH79spITol7xbHnlMYua1aMVKD5gZPgIhAL7EzHWOyfOBG0XDQbleRpH1zfdztq4X8dsNblSdxy+3","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22123},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"124cc132f3260fed8b8a5d0454c35f56c80ef03c","scripts":{"dev":"ts-node src/index.ts","build":"tsc","start":"node dist/index.js","prepare":"npm run build"},"_npmUser":{"name":"arc22-dev","email":"anishsh701@gmail.com"},"_npmVersion":"10.8.2","description":"Retry manager for handling multiple concurrent jobs","directories":{},"_nodeVersion":"18.20.8","_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.9.3","@types/node":"^24.6.1"},"_npmOperationalInternal":{"tmp":"tmp/retryq_1.0.0_1759352682140_0.5267936542790599","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@anishhs/retryq","version":"1.1.0","keywords":["retryq","retry","queue","manager","priority","jobs","asynchronous","concurrent","typescript","anishhs"],"author":{"name":"Anish Shekh"},"license":"ISC","_id":"@anishhs/retryq@1.1.0","maintainers":[{"name":"arc22-dev","email":"anishsh701@gmail.com"}],"dist":{"shasum":"4bcd2429cb696d7c8f41c06cf8b9ff6ed7076ed3","tarball":"https://registry.npmjs.org/@anishhs/retryq/-/retryq-1.1.0.tgz","fileCount":5,"integrity":"sha512-CokwtKaRHj4FQCkYlnYkZ+ZVtdguaMqOPrJKNQN+l3ZCFzYP1RStdFu+fuZV1K5BNG3d6oknTT0YeLRwyBjqfA==","signatures":[{"sig":"MEUCIQChstcrN2DDU1Xajy4fEhz32pT6DEZ1AjCSLvSIt1KN7wIgdPxuNaE6YIKIyR+Xj/HxWOBk+69buCV/oFF/f2JF6/8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":44688},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"6ccdddc56c1a08813209440865ac97432c800a44","scripts":{"dev":"ts-node src/index.ts","test":"npm run test:verification && npm run test:force-cancel && npm run test:cancellation","build":"tsc","start":"node dist/index.js","prepare":"npm run build","test:cancellation":"node tests/cancellation-proof.test.js","test:force-cancel":"node tests/force-cancellation.test.js","test:verification":"node tests/verification.test.js"},"_npmUser":{"name":"arc22-dev","email":"anishsh701@gmail.com"},"_npmVersion":"10.8.2","description":"Production-ready retry queue with force cancellation, priorities, and exponential backoff","directories":{},"_nodeVersion":"18.20.8","_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.9.3","@types/node":"^24.6.1"},"_npmOperationalInternal":{"tmp":"tmp/retryq_1.1.0_1767204107453_0.044892461108420934","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@anishhs/retryq","version":"1.2.0","description":"Production-ready retry queue with lifecycle events, force cancellation, priorities, and exponential backoff","main":"./dist/cjs/index.js","module":"./dist/esm/index.js","types":"./dist/cjs/index.d.ts","exports":{".":{"import":{"types":"./dist/esm/index.d.ts","default":"./dist/esm/index.js"},"require":{"types":"./dist/cjs/index.d.ts","default":"./dist/cjs/index.js"}},"./package.json":"./package.json"},"engines":{"node":">=16"},"scripts":{"clean":"rm -rf dist","build":"npm run clean && npm run build:cjs && npm run build:esm && node scripts/postbuild.js","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json","typecheck":"tsc -p tsconfig.json --noEmit","prepare":"npm run build","pretest":"npm run build","test":"node --test tests/"},"keywords":["retryq","retry","queue","manager","priority","jobs","events","asynchronous","concurrent","typescript","anishhs"],"author":{"name":"Anish Shekh"},"license":"ISC","devDependencies":{"@types/node":"^24.6.1","typescript":"^5.9.3"},"_id":"@anishhs/retryq@1.2.0","gitHead":"7bee4fed6a84eef12888b54f65b97fe97d024855","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-eqyPbDzyLld7gpW0dW0DD4mIsBsvt3zTPzPIho/S60/thnnE2q8SDYdB8ny9X6tVHHL7u+n1p2GCL8kyptnaVA==","shasum":"886ec77233693dffad86655f0fb0b0d9f4cf7a90","tarball":"https://registry.npmjs.org/@anishhs/retryq/-/retryq-1.2.0.tgz","fileCount":44,"unpackedSize":151792,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDmFIconme5Z6HlyAhaDKj3024ELjQdPnCJX+S6yzRUbgIgKJ21vvBOAO5rkvHqTiPicwWJP9HBfCIkzADgZ9p1i6Y="}]},"_npmUser":{"name":"arc22-dev","email":"anishsh701@gmail.com"},"directories":{},"maintainers":[{"name":"arc22-dev","email":"anishsh701@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/retryq_1.2.0_1782540563687_0.5996392853242412"},"_hasShrinkwrap":false}},"time":{"created":"2025-10-01T21:04:42.014Z","modified":"2026-06-27T06:09:23.926Z","1.0.0":"2025-10-01T21:04:42.334Z","1.1.0":"2025-12-31T18:01:47.592Z","1.2.0":"2026-06-27T06:09:23.816Z"},"author":{"name":"Anish Shekh"},"license":"ISC","keywords":["retryq","retry","queue","manager","priority","jobs","events","asynchronous","concurrent","typescript","anishhs"],"description":"Production-ready retry queue with lifecycle events, force cancellation, priorities, and exponential backoff","maintainers":[{"name":"arc22-dev","email":"anishsh701@gmail.com"}],"readme":"# @anishhs/retryq\n\nA production-ready, zero-dependency retry queue manager for Node.js with support for concurrent job execution, priorities, exponential backoff, jitter, and **force cancellation**.\n\n[![npm version](https://img.shields.io/npm/v/@anishhs/retryq.svg)](https://www.npmjs.com/package/@anishhs/retryq)\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\n[![License: ISC](https://img.shields.io/badge/License-ISC-green.svg)](https://opensource.org/licenses/ISC)\n\n## Features\n\n- ✅ **Concurrency control** - Limit concurrent job execution\n- ✅ **Priority queue** - Higher priority jobs execute first\n- ✅ **Exponential backoff** with configurable delay, multiplier, `maxDelay`, and jitter\n- ✅ **Lifecycle events & hooks** - `retry`/`success`/`failure`/`cancel`/`idle` events + per-job callbacks\n- ✅ **Conditional retries** - `shouldRetry(error, attempt)` predicate to skip non-retryable errors\n- ✅ **Force cancellation** - Abort in-progress jobs with AbortController\n- ✅ **Cooperative cancellation** - Graceful job termination\n- ✅ **Real time limits** - `maxTime` (and `attemptTimeout`) actively abort in-flight attempts\n- ✅ **Queue draining** - `onIdle()` / `drain()` to await all work\n- ✅ **Memory safe** - Bounded job history with LRU eviction\n- ✅ **Job introspection** - List, find, and track jobs by ID or label\n- ✅ **TypeScript** - Generic, fully-typed API with bundled declarations\n- ✅ **Dual ESM + CJS** - Ships both module formats with an `exports` map\n- ✅ **Zero dependencies** - Minimal footprint, no external packages\n\n## Installation\n\n```bash\nnpm install @anishhs/retryq\n```\n\n**Requirements**: Node.js 16+\n\n## Quick Start\n\n```typescript\nimport { RetryQManager } from '@anishhs/retryq';\n\n// Create manager with 3 concurrent jobs max\nconst retryQ = new RetryQManager({ maxConcurrent: 3 });\n\n// Create a job with retry logic\nconst job = retryQ.createJob(async (signal) => {\n  // Your async operation here\n  const response = await fetch('https://api.example.com/data', { signal });\n  return response.json();\n}, {\n  retries: 5,          // Retry up to 5 times\n  delay: 1000,         // Initial delay 1s\n  backoff: 2,          // Double delay each retry\n  jitter: 0.1,         // ±10% randomization\n  maxTime: 30000,      // Total timeout 30s\n  priority: 10,        // Higher priority = runs sooner\n  label: 'fetch-data'  // Human-readable identifier\n});\n\n// Wait for result\njob.promise\n  .then(data => console.log('Success:', data))\n  .catch(err => console.error('Failed:', err));\n\n// Cancel if needed\njob.cancel(true); // Force abort in-progress execution\n```\n\n## Table of Contents\n\n- [Core Concepts](#core-concepts)\n- [API Reference](#api-reference)\n- [Cancellation Modes](#cancellation-modes)\n- [Usage Examples](#usage-examples)\n- [Configuration Options](#configuration-options)\n- [Best Practices](#best-practices)\n- [Migration Guide](#migration-guide)\n- [Changelog](#changelog)\n\n---\n\n## Core Concepts\n\n### Job Lifecycle\n\n```\npending → running → completed\n                 → failed\n                 → cancelled\n```\n\n1. **Pending**: Job queued, waiting for available slot\n2. **Running**: Job executing with retries\n3. **Completed**: Job succeeded\n4. **Failed**: Job exhausted all retries\n5. **Cancelled**: Job cancelled by user\n\n### Retry Logic\n\n```\nAttempt 1: Execute immediately\n  ↓ (fails)\nAttempt 2: Wait delay * backoff^0 = 1000ms\n  ↓ (fails)\nAttempt 3: Wait delay * backoff^1 = 2000ms\n  ↓ (fails)\nAttempt 4: Wait delay * backoff^2 = 4000ms\n  ...\n```\n\nEach delay includes jitter: `delay ± (delay * jitter)`\n\n### Priority Queue\n\nJobs with higher `priority` values execute first:\n\n```typescript\nretryQ.createJob(taskA, { priority: 1 });  // Runs last\nretryQ.createJob(taskB, { priority: 5 });  // Runs second\nretryQ.createJob(taskC, { priority: 10 }); // Runs first\n```\n\n---\n\n## API Reference\n\n### RetryQManager\n\n#### Constructor\n\n```typescript\nnew RetryQManager(config?: RetryQManagerConfig | number)\n```\n\n**Parameters**:\n- `config.maxConcurrent` - Maximum concurrent jobs (default: `Infinity`)\n- `config.maxHistorySize` - Maximum jobs in history (default: `1000`)\n\n**Legacy**: Accepts number for `maxConcurrent` (backwards compatible)\n\n```typescript\n// New style (recommended)\nconst retryQ = new RetryQManager({\n  maxConcurrent: 5,\n  maxHistorySize: 1000\n});\n\n// Old style (still works)\nconst retryQ = new RetryQManager(5);\n```\n\n---\n\n#### createJob()\n\n```typescript\ncreateJob(\n  fn: (signal?: AbortSignal) => Promise<any>,\n  options?: RetryQJobOptions\n): RetryQJob\n```\n\n**Parameters**:\n- `fn` - Async function to execute\n  - `signal` - Optional AbortSignal for force cancellation\n- `options` - Job configuration (see [Configuration](#configuration-options))\n\n**Returns**: `RetryQJob` object\n\n```typescript\nconst job = retryQ.createJob(async (signal) => {\n  // Check signal to support force cancellation\n  if (signal?.aborted) throw new Error('Aborted');\n\n  return await doWork();\n}, {\n  retries: 3,\n  delay: 1000,\n  label: 'my-job'\n});\n```\n\n---\n\n#### cancelJob()\n\n```typescript\ncancelJob(id: string, force?: boolean): void\n```\n\n**Parameters**:\n- `id` - Job ID to cancel\n- `force` - Enable force cancellation (default: `false`)\n\n```typescript\n// Cooperative cancellation (default)\nretryQ.cancelJob(job.id);\n\n// Force cancellation (aborts via AbortSignal)\nretryQ.cancelJob(job.id, true);\n```\n\n---\n\n#### listJobs()\n\n```typescript\nlistJobs(): {\n  pending: JobSummary[];\n  running: JobSummary[];\n  failed: JobSummary[];\n  completed: JobSummary[];\n}\n```\n\n**Returns**: Snapshot of all jobs grouped by state\n\n```typescript\nconst { pending, running, failed, completed } = retryQ.listJobs();\nconsole.log(`${running.length} jobs currently executing`);\n```\n\n---\n\n#### findJobById()\n\n```typescript\nfindJobById(id: string): RetryQJob | null\n```\n\n**Returns**: Job if found, otherwise `null`\n\n```typescript\nconst job = retryQ.findJobById('job-123');\nif (job) {\n  console.log('Job state:', job.state);\n}\n```\n\n---\n\n#### findJobsByLabel()\n\n```typescript\nfindJobsByLabel(label: string): RetryQJob[]\n```\n\n**Returns**: Array of jobs with matching label\n\n```typescript\nconst emailJobs = retryQ.findJobsByLabel('send-email');\nconsole.log(`${emailJobs.length} email jobs found`);\n```\n\n---\n\n#### clearHistory()\n\n```typescript\nclearHistory(state?: JobState): void\n```\n\n**Parameters**:\n- `state` - Optional state to clear (`'failed'` or `'completed'`)\n- Omit to clear both\n\n```typescript\n// Clear completed jobs only\nretryQ.clearHistory('completed');\n\n// Clear all history\nretryQ.clearHistory();\n```\n\n---\n\n#### onIdle() / drain()\n\n```typescript\nonIdle(): Promise<void>\ndrain(): Promise<void>   // alias\n```\n\nResolves when the queue is fully idle (no pending **and** no running jobs).\nResolves immediately if already idle.\n\n```typescript\nfor (const item of items) {\n  retryQ.createJob(() => process(item), { retries: 3 });\n}\nawait retryQ.onIdle(); // wait for the whole batch to settle\n```\n\n---\n\n### Events\n\n`RetryQManager` extends Node's `EventEmitter` and emits typed events:\n\n```typescript\nretryQ.on('retry',   ({ job, info })   => console.log(`retry #${info.attempt} in ${info.nextDelay}ms`));\nretryQ.on('success', ({ job, result }) => console.log('done', job.label));\nretryQ.on('failure', ({ job, error })  => console.error('failed', job.label, error));\nretryQ.on('cancel',  ({ job })         => console.log('cancelled', job.label));\nretryQ.on('idle',    ()                => console.log('queue drained'));\n```\n\n| Event | Payload | Fired when |\n|-------|---------|-----------|\n| `retry` | `{ job, info: RetryInfo }` | A failed attempt schedules another try |\n| `success` | `{ job, result }` | A job completes successfully |\n| `failure` | `{ job, error }` | A job fails terminally |\n| `cancel` | `{ job }` | A job is cancelled |\n| `idle` | _(none)_ | The queue transitions to fully idle |\n\nPrefer per-job feedback? Use the `onRetry` / `onSuccess` / `onFailure` /\n`onCancel` callbacks in `RetryQJobOptions`.\n\n---\n\n### Conditional Retries\n\nSkip retries for errors that will never succeed:\n\n```typescript\nretryQ.createJob(async (signal) => {\n  const res = await fetch(url, { signal });\n  if (!res.ok) throw Object.assign(new Error('HTTP'), { status: res.status });\n  return res.json();\n}, {\n  retries: 5,\n  // Retry 5xx and network errors; give up on 4xx immediately.\n  shouldRetry: (err) => !(err?.status >= 400 && err?.status < 500),\n});\n```\n\n---\n\n### RetryQJob Interface\n\n```typescript\ninterface RetryQJob {\n  id: string;                           // Unique identifier\n  label: string;                        // Human-readable name\n  state: JobState;                      // Current state\n  priority: number;                     // Execution priority\n  retriesLeft: number;                  // Remaining attempts\n  promise: Promise<any>;                // Result promise\n  cancel: (force?: boolean) => void;    // Cancel method\n  fn: (signal?: AbortSignal) => Promise<any>;\n  options: RetryQJobOptions;            // Configuration\n  createdAt: number;                    // Timestamp (ms)\n  startedAt?: number;                   // Execution start (ms)\n  finishedAt?: number;                  // Completion time (ms)\n  error?: any;                          // Last error\n  abortController?: AbortController;    // Internal controller\n}\n```\n\n---\n\n## Cancellation Modes\n\n### 1. Cooperative Cancellation (Default)\n\n**Usage**: `job.cancel()` or `job.cancel(false)`\n\n**Behavior**:\n- ✅ Prevents future retries\n- ✅ Interrupts sleep between retries\n- ❌ Does NOT abort in-progress execution\n\n**When to use**:\n- Operations should complete cleanly\n- Legacy code without signal support\n- Database transactions\n\n```typescript\nconst job = retryQ.createJob(async () => {\n  await database.transaction();\n  return 'done';\n});\n\njob.cancel(); // Waits for transaction to complete\n```\n\n---\n\n### 2. Force Cancellation ⭐ NEW!\n\n**Usage**: `job.cancel(true)`\n\n**Behavior**:\n- ✅ Prevents future retries\n- ✅ Interrupts sleep between retries\n- ✅ **Aborts in-progress execution via AbortSignal**\n\n**When to use**:\n- HTTP requests (fetch, axios)\n- Long-running computations\n- File uploads/downloads\n- Polling operations\n\n```typescript\nconst job = retryQ.createJob(async (signal) => {\n  // Check signal to enable force abort\n  for (let i = 0; i < 1000; i++) {\n    if (signal?.aborted) throw new Error('Aborted');\n    await processItem(i);\n  }\n});\n\njob.cancel(true); // Immediately aborts execution\n```\n\n---\n\n### External AbortController\n\nLink your own `AbortController` to the job:\n\n```typescript\nconst controller = new AbortController();\n\nconst job = retryQ.createJob(async (signal) => {\n  return await longOperation(signal);\n}, {\n  signal: controller.signal  // Link external signal\n});\n\n// Cancel via external controller\ncontroller.abort();\n\n// Or via job method\njob.cancel(true);\n```\n\n---\n\n## Usage Examples\n\n### Example 1: HTTP Requests with Retries\n\n```typescript\nasync function fetchWithRetry(url: string) {\n  const retryQ = new RetryQManager({ maxConcurrent: 5 });\n\n  const job = retryQ.createJob(async (signal) => {\n    const response = await fetch(url, { signal });\n\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`);\n    }\n\n    return response.json();\n  }, {\n    retries: 5,\n    delay: 1000,\n    backoff: 2,\n    jitter: 0.15,\n    maxTime: 30000,\n    label: 'fetch-api'\n  });\n\n  return job.promise;\n}\n\n// Use it\nconst data = await fetchWithRetry('https://api.example.com/data');\n```\n\n---\n\n### Example 2: Batch Processing with Priority\n\n```typescript\nconst retryQ = new RetryQManager({ maxConcurrent: 3 });\n\nconst users = ['user1', 'user2', 'user3'];\n\nfor (const userId of users) {\n  retryQ.createJob(async (signal) => {\n    if (signal?.aborted) throw new Error('Aborted');\n    return await syncUser(userId);\n  }, {\n    label: `sync-${userId}`,\n    priority: userId === 'admin' ? 10 : 5, // Admin first\n    retries: 3\n  });\n}\n```\n\n---\n\n### Example 3: File Upload with Progress Tracking\n\n```typescript\nconst uploadJob = retryQ.createJob(async (signal) => {\n  const formData = new FormData();\n  formData.append('file', fileBlob);\n\n  const response = await fetch('/upload', {\n    method: 'POST',\n    body: formData,\n    signal // Abort upload on cancel\n  });\n\n  return response.json();\n}, {\n  retries: 3,\n  delay: 2000,\n  label: 'file-upload'\n});\n\n// User clicks cancel button\ncancelButton.onclick = () => uploadJob.cancel(true);\n\n// Track progress\nuploadJob.promise\n  .then(result => console.log('Upload complete:', result))\n  .catch(err => console.log('Upload failed:', err.message));\n```\n\n---\n\n### Example 4: Polling with Auto-Stop\n\n```typescript\nconst pollJob = retryQ.createJob(async (signal) => {\n  while (true) {\n    if (signal?.aborted) throw new Error('Polling stopped');\n\n    const status = await checkJobStatus(signal);\n\n    if (status === 'completed') {\n      return status;\n    }\n\n    await new Promise(resolve => setTimeout(resolve, 5000));\n  }\n}, {\n  retries: 100,\n  delay: 5000,\n  maxTime: 300000, // 5 minutes total\n  label: 'poll-job-status'\n});\n\n// Stop polling\nsetTimeout(() => pollJob.cancel(true), 60000);\n```\n\n---\n\n### Example 5: Graceful Shutdown\n\n```typescript\nconst jobs: RetryQJob[] = [];\n\n// Queue multiple jobs\nfor (let i = 0; i < 100; i++) {\n  const job = retryQ.createJob(async (signal) => {\n    return await processItem(i, signal);\n  }, { retries: 3 });\n\n  jobs.push(job);\n}\n\n// Handle shutdown signal\nprocess.on('SIGTERM', async () => {\n  console.log('Shutting down gracefully...');\n\n  // Cancel all running jobs cooperatively\n  jobs.forEach(job => {\n    if (job.state === 'running' || job.state === 'pending') {\n      job.cancel(); // Cooperative\n    }\n  });\n\n  // Wait for jobs to finish (with timeout)\n  await Promise.race([\n    Promise.allSettled(jobs.map(j => j.promise)),\n    new Promise(resolve => setTimeout(resolve, 10000))\n  ]);\n\n  process.exit(0);\n});\n```\n\n---\n\n## Configuration Options\n\n### RetryQJobOptions\n\n```typescript\ntype RetryQJobOptions<T = unknown> = {\n  retries?: number;        // Number of retry attempts (default: 3)\n  delay?: number;          // Initial delay in ms (default: 1000)\n  backoff?: number;        // Delay multiplier (default: 2)\n  maxTime?: number;        // Total time limit in ms (default: 30000)\n  maxDelay?: number;       // Cap for a single backoff delay (default: Infinity)\n  attemptTimeout?: number; // Per-attempt timeout in ms (default: Infinity)\n  jitter?: number;         // Jitter fraction 0-1 (default: 0.1)\n  label?: string;          // Human-readable identifier (default: job ID)\n  priority?: number;       // Execution priority (default: 1)\n  signal?: AbortSignal;    // External abort signal (optional)\n\n  // Conditional retry: return false to stop retrying immediately\n  shouldRetry?: (error: unknown, attempt: number) => boolean;\n\n  // Per-job lifecycle callbacks\n  onRetry?: (info: RetryInfo) => void;\n  onSuccess?: (result: T) => void;\n  onFailure?: (error: unknown) => void;\n  onCancel?: () => void;\n};\n```\n\n### Default Values\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `retries` | `3` | Number of retry attempts after initial try |\n| `delay` | `1000` | Initial delay between retries (ms) |\n| `backoff` | `2` | Multiplier for exponential backoff |\n| `maxTime` | `30000` | Total execution time limit (30s), enforced during attempts |\n| `maxDelay` | `Infinity` | Cap for a single backoff delay (ms) |\n| `attemptTimeout` | `Infinity` | Per-attempt timeout (ms) |\n| `jitter` | `0.1` | Random delay variation (±10%) |\n| `priority` | `1` | Queue priority (higher = sooner) |\n| `maxConcurrent` | `Infinity` | Concurrent job limit |\n| `maxHistorySize` | `1000` | Jobs kept in history per state |\n\n### Validation Rules\n\n```typescript\n// retries: 0 to 100\nif (retries < 0) throw new Error('retries must be >= 0');\nif (retries > 100) throw new Error('retries cannot exceed 100 (DoS protection)');\n\n// delay: >= 0\nif (delay < 0) throw new Error('delay must be >= 0');\n\n// backoff: >= 1\nif (backoff < 1) throw new Error('backoff must be >= 1');\n\n// maxTime: > 0\nif (maxTime <= 0) throw new Error('maxTime must be > 0');\n\n// jitter: 0 to 1\nif (jitter < 0 || jitter > 1) throw new Error('jitter must be between 0 and 1');\n```\n\n---\n\n## Best Practices\n\n### ✅ DO\n\n**1. Use AbortSignal for force cancellation**\n```typescript\nasync (signal) => {\n  if (signal?.aborted) throw new Error('Aborted');\n  await work();\n}\n```\n\n**2. Set appropriate maxTime**\n```typescript\n// Long operations need higher limits\nretryQ.createJob(fn, { maxTime: 60000 }); // 1 minute\n```\n\n**3. Use labels for tracking**\n```typescript\nretryQ.createJob(fn, { label: 'user-sync:123' });\n```\n\n**4. Clean up history periodically**\n```typescript\nsetInterval(() => retryQ.clearHistory('completed'), 3600000); // Hourly\n```\n\n**5. Monitor queue depth**\n```typescript\nconst { pending, running } = retryQ.listJobs();\nconsole.log(`Queue: ${pending.length} pending, ${running.length} running`);\n```\n\n---\n\n### ❌ DON'T\n\n**1. Don't ignore signal parameter**\n```typescript\n// BAD - force cancel won't work\nasync () => await work();\n\n// GOOD - supports force cancel\nasync (signal) => {\n  if (signal?.aborted) throw new Error('Aborted');\n  await work();\n}\n```\n\n**2. Don't use infinite retries**\n```typescript\n// BAD - will retry forever\n{ retries: Infinity }\n\n// GOOD - capped at 100\n{ retries: 10 }\n```\n\n**3. Don't leak secrets in errors**\n```typescript\n// BAD - error might contain API key\nthrow new Error(`Failed with key: ${apiKey}`);\n\n// GOOD - sanitized error\nthrow new Error('API request failed');\n```\n\n---\n\n## Migration Guide\n\n### From v1.1.x to v1.2.x\n\n**No breaking API changes.** All existing code keeps working; new options,\ncallbacks, events, and methods are additive. Two behavior fixes to be aware of:\n\n- `listJobs().cancelled` now holds cancelled jobs — they no longer appear under\n  `failed`.\n- `maxTime` now actively aborts an in-flight attempt once the budget is\n  exhausted (previously it only blocked starting a new attempt).\n\nThe package now ships **both ESM and CJS** with an `exports` map; `import` and\n`require` both resolve automatically.\n\n### From v1.0.x to v1.1.x\n\n**No breaking changes!** All existing code works.\n\n**To add force cancellation**:\n\n```typescript\n// Before (v1.0.x)\nconst job = retryQ.createJob(async () => {\n  await work();\n});\n\n// After (v1.1.x with force cancel)\nconst job = retryQ.createJob(async (signal) => {\n  if (signal?.aborted) throw new Error('Aborted');\n  await work();\n});\n\njob.cancel(true); // Now supports force abort!\n```\n\n---\n\n## Performance\n\n### Benchmarks\n\n**Tested on**: MacBook Pro M1, 16GB RAM, Node.js 20\n\n| Operation | Performance |\n|-----------|------------|\n| Create 1000 jobs | ~5ms |\n| ID collision (1000 concurrent) | 0 collisions |\n| Signal check (1M iterations) | ~2-3ms |\n| Queue processing (100 jobs) | <1ms |\n| Memory usage (10K jobs) | ~50MB |\n\n### Memory Management\n\n- **Bounded history**: LRU eviction at `maxHistorySize`\n- **Registry cleanup**: Automatic cleanup after job completion\n- **No leaks**: All references cleaned up properly\n\n---\n\n## TypeScript Support\n\nFull type safety with bundled declarations:\n\n```typescript\nimport {\n  RetryQManager,\n  RetryQJob,\n  RetryQJobOptions,\n  RetryQManagerConfig,\n  JobState,\n  CancelableFunction\n} from '@anishhs/retryq';\n\nconst manager: RetryQManager = new RetryQManager({\n  maxConcurrent: 5,\n  maxHistorySize: 1000\n});\n\nconst job: RetryQJob = manager.createJob(\n  async (signal?: AbortSignal) => {\n    return 'result';\n  },\n  {\n    retries: 3,\n    delay: 1000\n  }\n);\n```\n\n---\n\n## Troubleshooting\n\n### Issue: Jobs not executing\n\n**Cause**: Exceeded `maxConcurrent` limit\n\n**Solution**: Increase limit or wait for jobs to complete\n```typescript\nnew RetryQManager({ maxConcurrent: 10 }); // Increase from default\n```\n\n---\n\n### Issue: Memory growing unbounded\n\n**Cause**: Too many jobs in history\n\n**Solution**: Lower `maxHistorySize` or clear history\n```typescript\nnew RetryQManager({ maxHistorySize: 500 }); // Lower limit\nretryQ.clearHistory(); // Manual cleanup\n```\n\n---\n\n### Issue: Force cancel not working\n\n**Cause**: Job function doesn't check signal\n\n**Solution**: Add signal checks\n```typescript\nasync (signal) => {\n  if (signal?.aborted) throw new Error('Aborted');\n  // ... your code\n}\n```\n\n---\n\n## FAQ\n\n**Q: Is this production-ready?**\nA: Yes — covered by a `node:test` suite spanning concurrency, cancellation, events, timeouts, and retry semantics.\n\n**Q: Does it work with TypeScript?**\nA: Yes, full TypeScript support with bundled type definitions.\n\n**Q: Can I use this in serverless (Lambda)?**\nA: Yes, but jobs are in-memory only. They won't persist across cold starts.\n\n**Q: Does it support distributed systems?**\nA: No, it's single-process only. For distributed queues, use Redis/RabbitMQ.\n\n**Q: What's the difference between cooperative and force cancellation?**\nA: Cooperative prevents retries but allows current execution to complete. Force uses AbortSignal to interrupt in-progress execution.\n\n**Q: Can I use this with fetch/axios?**\nA: Yes! Pass the signal parameter directly to fetch() or axios.\n\n---\n\n## Examples Repository\n\nMore examples available at: [github.com/anishhs-gh/retryq-examples](https://github.com/anishhs-gh/retryq-examples) *(coming soon)*\n\n---\n\n## Development\n\n```bash\nnpm install        # install dev dependencies\nnpm run typecheck  # tsc --noEmit\nnpm run build      # emit dual ESM + CJS into dist/ (with .d.ts)\nnpm test           # build (pretest) then run node:test suite\n```\n\nThe package builds to both module formats:\n\n- CommonJS → `dist/cjs` (`require`)\n- ES modules → `dist/esm` (`import`)\n\nresolved automatically via the `exports` map in `package.json`.\n\n## CI / Release\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| **Test** | push/PR to `develop`/`master` | Typecheck, build, and test on Node 16/18/20 |\n| **Audit** | push/PR + weekly | `npm audit` of production dependencies |\n| **Dry-run publish** | push to `develop`, PRs | Gated on Test + Audit; verifies the npm token authenticates and has publish permission, and that `npm publish` would succeed — without publishing |\n| **Publish (manual)** | `workflow_dispatch` | Gated on Test + Audit; publishes to npm and creates a GitHub Release + version tag |\n\nReleases are **manual**: bump the version in `package.json`, merge to `master`,\nthen run the **Publish** workflow from the Actions tab. Requires an `NPM_TOKEN`\nrepository secret with publish access to the `@anishhs` scope.\n\n## Contributing\n\nContributions welcome! Please:\n1. Fork the repository\n2. Create a feature branch (off `develop`)\n3. Add tests for new features (`tests/*.test.js`, `node:test`)\n4. Ensure `npm test` passes\n5. Submit a pull request into `develop`\n\n---\n\n## License\n\nISC © Anish Shekh\n\n---\n\n## Changelog\n\nSee [CHANGELOG.md](./CHANGELOG.md) for version history.\n\n---\n\n## Support\n\n- **Issues**: [GitHub Issues](https://github.com/anishhs-gh/retryq/issues)\n- **GitHub**: [anishhs-gh](https://github.com/anishhs-gh)\n- **Website**: [anishhs.com](https://anishhs.com)\n- **LinkedIn**: [linkedin.com/in/anishsh](https://linkedin.com/in/anishsh)\n\n---\n\n**Made with ❤️ by Anish Shekh**\n","readmeFilename":"README.md"}