{"_id":"@ehealth-co-id/typescript-retry-decorator","_rev":"7-bc650d55858bc65a45845d913215c4f9","name":"@ehealth-co-id/typescript-retry-decorator","dist-tags":{"latest":"3.0.1"},"versions":{"2.0.6":{"name":"@ehealth-co-id/typescript-retry-decorator","version":"2.0.6","keywords":["typescirpt","decorator","retry"],"author":{"name":"Han Li"},"license":"MIT","_id":"@ehealth-co-id/typescript-retry-decorator@2.0.6","maintainers":[{"name":"indahreforsiana","email":"indahreforsiana@gmail.com"},{"name":"ibrohimislam","email":"ibrohimislam@gmail.com"}],"homepage":"https://github.com/vcfvct/typescript-retry-decorator#readme","bugs":{"url":"https://github.com/vcfvct/typescript-retry-decorator/issues"},"dist":{"shasum":"347e5580430bd2eccd2b550646ab4f066862372f","tarball":"https://registry.npmjs.org/@ehealth-co-id/typescript-retry-decorator/-/typescript-retry-decorator-2.0.6.tgz","fileCount":33,"integrity":"sha512-Nx0WR/UqmumG4Rq/5K07pw5W0cfrs7yK3YTZLlEMeQav/VIZc/gELn2JUuIhfG6JO1adRZIO8x1ZfXpQd8YJ/g==","signatures":[{"sig":"MEYCIQD7F546Rz4Phm4sAz6BQutFUWztIxxFPycPcDywI039PgIhAPSuSLYaDBJZ/6OPuFz9WA1iJbl4mUNODNiY4v8N9j5q","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":60898,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjBwBcACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmpzsw/+Ja2HGttR1whLYnSjgBstyT5rGbrt94DzDw/JueV3l5oEj0Cr\r\n2rtGAoenm0cz3Jg9fk+USWoiWuiS2EcKIXlB81rGcJoE50FD9E0ZIGt4/Zod\r\nI7kNkG0Q6YZnivNa3I7Ob3xNh+nTAp1jHI9WJBbrUC5zK6gkHA6EmhqIyDm9\r\nogHW86TA8jo+WEKCfhWSeCvRb9ldCHlfD2VGqvHmvT87jtzEgwo2V4AGOqfI\r\ncBvk56SYudVJdC+JMq1UwcwsJ+g8Bxj0QlyrQlc529dBcfMiQWiAkWF0Yzmv\r\nGr4SXzltwVqiX/b/3yyZCV9ysKGBpKBRGdF6YJH265x693fjVw4ZAFrJvXC1\r\nIomYjbuVfv0CXXbfV7asGKq5yB1LeZo+v+OsoI5pWVzxUWcRoJJK8jo7tBM8\r\n87lM60MFEUQ/OR2iYUlvm00olKvZ2Gn54fk3xA2LJr0kMjYLpPvx0K8x3g99\r\nwrUvaBjSkGhB7T2dfgwEmMEXtQbmtJUP9U9C0q7MKPIy4ViFn5ecQyDKDez+\r\nVolqrluftcR7gUCUDYtplgfqxSpUqO7GobKWbCGVMDEwPKdCE1tQ+7KyO/MA\r\n7LTt0b2OX7v8s/8YhYvXmkq2lKAQkwnz8gzJReshhecjzhv1FXN0BGsE058l\r\nKOttvoHtSwa0a42AwpUO5eoZfWlwdHO4jcI=\r\n=f7mX\r\n-----END PGP SIGNATURE-----\r\n"},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"65800c5b53757acf1b294b498d43db8895244d01","scripts":{"lint":"eslint --ext .ts src/","test":"jest","build":"tsc --declaration --project .","example":"ts-node src/example.ts"},"typings":"dist/index.d.ts","_npmUser":{"name":"indahreforsiana","email":"indahreforsiana@gmail.com"},"repository":{"url":"git+https://github.com/vcfvct/typescript-retry-decorator.git","type":"git"},"_npmVersion":"8.1.2","description":"A simple retry decorator for typescript with no dependency.","directories":{},"_nodeVersion":"16.13.2","_hasShrinkwrap":false,"devDependencies":{"jest":"^28.1.1","eslint":"^8.17.0","ts-jest":"^28.0.4","ts-node":"^10.8.1","typescript":"^4.7.3","@types/jest":"^28.1.1","@types/node":"^17.0.41","eslint-config-ts-vcfvct":"^1.0.0","@typescript-eslint/parser":"^5.27.1","@typescript-eslint/eslint-plugin":"^5.27.1"},"_npmOperationalInternal":{"tmp":"tmp/typescript-retry-decorator_2.0.6_1661403228749_0.26250830453705065","host":"s3://npm-registry-packages"}},"3.0.1":{"name":"@ehealth-co-id/typescript-retry-decorator","version":"3.0.1","description":"A simple retry decorator for typescript with no dependency.","main":"dist/index.js","types":"dist/index.d.ts","typings":"dist/index.d.ts","scripts":{"test":"jest","example":"ts-node src/example.ts","lint":"eslint --ext .ts src/","build":"tsc --declaration --project ."},"repository":{"type":"git","url":"git+https://github.com/ehealth-co-id/typescript-retry-decorator.git"},"keywords":["typescript","decorator","resilience","retry"],"author":{"name":"Han Li"},"license":"MIT","bugs":{"url":"https://github.com/ehealth-co-id/typescript-retry-decorator/issues"},"homepage":"https://github.com/ehealth-co-id/typescript-retry-decorator#readme","devDependencies":{"@types/jest":"^28.1.1","@types/node":"^17.0.41","@typescript-eslint/eslint-plugin":"^5.27.1","@typescript-eslint/parser":"^5.27.1","eslint":"^8.57.1","eslint-config-ts-vcfvct":"^1.0.0","jest":"^28.1.1","ts-jest":"^28.0.4","ts-node":"^10.8.1","typescript":"^5.9.3"},"_id":"@ehealth-co-id/typescript-retry-decorator@3.0.1","gitHead":"9ba5f1804e0c6cacaa962492d71fcef3eea6e8d3","_nodeVersion":"21.6.1","_npmVersion":"10.2.4","dist":{"integrity":"sha512-Ydj8kSp5odObMiNYssd28S/Q6pzi1x3SzVtlsfC2y8b9fUyazKJBHFgYifkgi3mrel0HLh9uFpRkht3WVze9rQ==","shasum":"de8b6c531750100e49fe2632777d39ba65fbd9c2","tarball":"https://registry.npmjs.org/@ehealth-co-id/typescript-retry-decorator/-/typescript-retry-decorator-3.0.1.tgz","fileCount":28,"unpackedSize":132391,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDAPY359M8HMv0SVmxHLD+FLs8FQZJ7nTwqO6+yUqEVNQIhAPB//YetQAuVOrfG3YPiq75WrDy1KvwT/B88vXJQgoHJ"}]},"_npmUser":{"name":"ibrohimislam","email":"ibrohimislam@gmail.com"},"directories":{},"maintainers":[{"name":"ibrohimislam","email":"ibrohimislam@gmail.com"},{"name":"indahreforsiana","email":"indahreforsiana@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/typescript-retry-decorator_3.0.1_1762837706603_0.4524799696799282"},"_hasShrinkwrap":false}},"time":{"created":"2022-08-25T04:53:48.696Z","modified":"2025-11-11T05:08:27.039Z","2.0.5":"2022-08-25T04:42:54.886Z","2.0.6":"2022-08-25T04:53:48.904Z","3.0.0":"2025-11-11T04:56:46.624Z","3.0.1":"2025-11-11T05:08:26.793Z"},"bugs":{"url":"https://github.com/ehealth-co-id/typescript-retry-decorator/issues"},"author":{"name":"Han Li"},"license":"MIT","homepage":"https://github.com/ehealth-co-id/typescript-retry-decorator#readme","keywords":["typescript","decorator","resilience","retry"],"repository":{"type":"git","url":"git+https://github.com/ehealth-co-id/typescript-retry-decorator.git"},"description":"A simple retry decorator for typescript with no dependency.","maintainers":[{"name":"ibrohimislam","email":"ibrohimislam@gmail.com"},{"name":"indahreforsiana","email":"indahreforsiana@gmail.com"}],"readme":"![Retry](https://cdn.iconscout.com/icon/free/png-256/retry-1-386755.png)\n## A simple retry decorator for typescript with 0 dependency.\nThis is inspired by the [Spring-Retry project](https://github.com/spring-projects/spring-retry). Written in Typescript, *100%* Test Coverage.\n\nImport and use it. Retry for `Promise` is supported as long as the `runtime` has promise(nodejs/evergreen-browser).\n\n**Features:**\n- 🎯 Use as decorator (`@Retryable`) or function wrapper (`withRetry`)\n- ⏱️ Fixed and exponential backoff strategies\n- 🎲 Jitter support (full, equal, decorrelated) to prevent thundering herd\n- 🚫 Cancellable with `AbortSignal` support\n- 🎨 Conditional retry with custom logic\n- 📦 Zero dependencies\n- 💯 100% test coverage\n\n### Install\n> npm install typescript-retry-decorator\n\n### Options\n| Option Name       | Type                  | Required? | Default                                 | Description                                                                                                       |\n|:-----------------:|:------:|:---------:|:---------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------:|\n| maxAttempts       | number                | Yes       | -                                       | The max attempts to try                                                                                           |\n| backOff           | number                | No        | 0                                       | number in `ms` to back off.  If not set, then no wait                                                             |\n| backOffPolicy     | enum                  | No        | FixedBackOffPolicy                      | can be fixed or exponential                                                                                       |\n| exponentialOption | object                | No        | { maxInterval: 2000,    multiplier: 2 } | This is for the `ExponentialBackOffPolicy` <br/> The max interval each wait and the multiplier for the `backOff`. |\n| doRetry           | (e: any) => boolean   | No        | -                                       | Function with error parameter to decide if repetition is necessary.                                               |\n| value             | Error/Exception class | No        | [ ]                                     | An array of Exception types that are retryable.                                                                   |\n| reraise           | boolean               | No        | false                                   | If `true`, rethrows the original error instead of `MaxAttemptsError` when max attempts is reached.                 |\n| signal            | AbortSignal           | No        | -                                       | An `AbortSignal` to cancel the retry operation. Throws `AbortError` when aborted.                                 |\n| useJitter         | boolean               | No        | false                                   | If `true`, adds random jitter to backoff duration to prevent thundering herd problem.                              |\n| jitterType        | 'full' \\| 'equal' \\| 'decorrelated' | No | 'full'                          | Type of jitter: `full` (0 to backOff), `equal` (backOff/2 to backOff), `decorrelated` (backOff to 3×backOff).     |\n\n## Usage\n\n### 1. As a Decorator\n\nUse `@Retryable` decorator on class methods:\n\n```typescript\nimport { Retryable, BackOffPolicy } from 'typescript-retry-decorator';\n\nclass ApiService {\n  @Retryable({ maxAttempts: 3 })\n  async fetchData(url: string) {\n    // This method will be retried up to 3 times on failure\n    const response = await fetch(url);\n    if (!response.ok) throw new Error('Failed to fetch');\n    return response.json();\n  }\n\n  @Retryable({ \n    maxAttempts: 3, \n    backOff: 1000,\n    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy\n  })\n  async uploadFile(file: File) {\n    // Retries with exponential backoff: 1s, 2s, 4s\n    const formData = new FormData();\n    formData.append('file', file);\n    const response = await fetch('/upload', { method: 'POST', body: formData });\n    if (!response.ok) throw new Error('Upload failed');\n    return response.json();\n  }\n}\n```\n\n### 2. As a Function Wrapper\n\nUse `withRetry` to wrap any function:\n\n```typescript\nimport { withRetry, BackOffPolicy } from 'typescript-retry-decorator';\n\n// Wrap an existing function\nasync function fetchUser(userId: string) {\n  const response = await fetch(`/api/users/${userId}`);\n  if (!response.ok) throw new Error('Failed to fetch user');\n  return response.json();\n}\n\nconst fetchUserWithRetry = withRetry(\n  { maxAttempts: 3, backOff: 1000 },\n  fetchUser\n);\n\n// Use it\nconst user = await fetchUserWithRetry('123');\n\n// Or wrap inline\nconst processWithRetry = withRetry(\n  { maxAttempts: 5, backOff: 2000 },\n  async (data: string) => {\n    // Your async operation here\n    return await someAsyncOperation(data);\n  }\n);\n```\n\n## Examples\n\n### Basic Retry\n```typescript\nimport { Retryable, withRetry } from 'typescript-retry-decorator';\n\n// Decorator style\nclass Service {\n  @Retryable({ maxAttempts: 3 })\n  async fetchData() {\n    throw new Error('I failed!');\n  }\n}\n\n// Function wrapper style\nconst fetchData = withRetry(\n  { maxAttempts: 3 },\n  async () => {\n    throw new Error('I failed!');\n  }\n);\n```\n\n### Retry with Backoff\n```typescript\n// Fixed backoff - wait 1 second between retries\n@Retryable({\n  maxAttempts: 3,\n  backOffPolicy: BackOffPolicy.FixedBackOffPolicy,\n  backOff: 1000\n})\nasync fixedBackOffRetry() {\n  throw new Error('I failed!');\n}\n\n// Exponential backoff - wait 1s, 3s, 9s\n@Retryable({\n  maxAttempts: 3,\n  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n  backOff: 1000,\n  exponentialOption: { maxInterval: 10000, multiplier: 3 }\n})\nasync exponentialBackOffRetry() {\n  throw new Error('I failed!');\n}\n```\n\n### Retry Specific Errors\n```typescript\n// Only retry on specific error types\n@Retryable({ \n  maxAttempts: 3, \n  value: [SyntaxError, ReferenceError]\n})\nasync retrySpecificErrors() {\n  throw new SyntaxError('This will retry');\n  // throw new TypeError('This will NOT retry');\n}\n```\n\n### Conditional Retry\n```typescript\n// Retry only when custom condition is met\n@Retryable({ \n  maxAttempts: 3,\n  backOff: 1000,\n  doRetry: (e: Error) => {\n    // Only retry on 429 (Too Many Requests) or 503 (Service Unavailable)\n    return e.message.includes('429') || e.message.includes('503');\n  }\n})\nasync conditionalRetry() {\n  throw new Error('Error: 429 Too Many Requests');\n}\n```\n\n### Reraise Original Error\n```typescript\n// By default, MaxAttemptsError is thrown with the original error wrapped\n// Use reraise: true to throw the original error instead\n@Retryable({ \n  maxAttempts: 3,\n  reraise: true  // Throw original error, not MaxAttemptsError\n})\nasync reraiseExample() {\n  throw new Error('Original error');\n}\n\ntry {\n  await service.reraiseExample();\n} catch (error) {\n  // error is the original Error, not MaxAttemptsError\n  console.log(error.message); // \"Original error\"\n}\n```\n\n### Jitter to Prevent Thundering Herd\n```typescript\nimport { withRetry, BackOffPolicy } from 'typescript-retry-decorator';\n\n// Full Jitter - Random backoff between 0 and backOff duration\n// Provides maximum randomization to spread out retry attempts\nconst fetchWithFullJitter = withRetry(\n  { \n    maxAttempts: 5, \n    backOff: 2000,\n    useJitter: true,\n    jitterType: 'full'  // Backoff will be 0-2000ms randomly\n  },\n  async (url: string) => {\n    const response = await fetch(url);\n    if (!response.ok) throw new Error('Failed');\n    return response.json();\n  }\n);\n\n// Equal Jitter - Random backoff between backOff/2 and backOff\n// Maintains minimum wait time while adding randomness\nconst fetchWithEqualJitter = withRetry(\n  { \n    maxAttempts: 5, \n    backOff: 2000,\n    useJitter: true,\n    jitterType: 'equal'  // Backoff will be 1000-2000ms randomly\n  },\n  fetchData\n);\n\n// Decorrelated Jitter - Can increase backoff beyond base duration\n// More aggressive randomization for heavily loaded systems\nconst fetchWithDecorrelatedJitter = withRetry(\n  { \n    maxAttempts: 5, \n    backOff: 1000,\n    useJitter: true,\n    jitterType: 'decorrelated'  // Backoff will be 1000-3000ms randomly\n  },\n  fetchData\n);\n\n// Jitter with Exponential Backoff\n// Combines exponential growth with randomization\n@Retryable({\n  maxAttempts: 5,\n  backOff: 1000,\n  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n  exponentialOption: { maxInterval: 30000, multiplier: 2 },\n  useJitter: true,\n  jitterType: 'full'\n})\nasync apiCallWithJitter() {\n  // First retry: 0-1000ms\n  // Second retry: 0-2000ms\n  // Third retry: 0-4000ms\n  // etc.\n}\n```\n\n### Cancellable Retry with AbortSignal\n```typescript\nimport { withRetry, AbortError } from 'typescript-retry-decorator';\n\n// Create an abort controller\nconst controller = new AbortController();\n\n// Function wrapper with signal\nconst fetchWithRetry = withRetry(\n  { \n    maxAttempts: 10, \n    backOff: 2000,\n    signal: controller.signal  // Pass the abort signal\n  },\n  async (url: string) => {\n    const response = await fetch(url);\n    if (!response.ok) throw new Error('Failed');\n    return response.json();\n  }\n);\n\n// Cancel after 5 seconds\nsetTimeout(() => controller.abort(), 5000);\n\ntry {\n  const data = await fetchWithRetry('https://api.example.com/data');\n} catch (error) {\n  if (error instanceof AbortError) {\n    console.log('Retry operation was cancelled');\n  }\n}\n\n// Also works with decorator\nconst controller2 = new AbortController();\n\nclass Service {\n  @Retryable({ \n    maxAttempts: 5, \n    backOff: 1000,\n    signal: controller2.signal \n  })\n  async fetchData() {\n    // Will be cancelled when controller2.abort() is called\n  }\n}\n```\n\n### Real-world Example\n```typescript\nimport { withRetry, BackOffPolicy, MaxAttemptsError, AbortError } from 'typescript-retry-decorator';\n\nclass ApiClient {\n  private baseUrl = 'https://api.example.com';\n\n  // Decorator on class method\n  @Retryable({\n    maxAttempts: 3,\n    backOff: 1000,\n    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n    exponentialOption: { maxInterval: 5000, multiplier: 2 },\n    doRetry: (e: Error) => {\n      // Retry on network errors or 5xx server errors\n      return e.message.includes('network') || e.message.includes('5');\n    }\n  })\n  async get(endpoint: string) {\n    const response = await fetch(`${this.baseUrl}${endpoint}`);\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`);\n    }\n    return response.json();\n  }\n\n  // Function wrapper with cancellation\n  async getWithCancellation(endpoint: string, timeoutMs: number) {\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n    const fetchWithRetry = withRetry(\n      {\n        maxAttempts: 5,\n        backOff: 1000,\n        backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n        signal: controller.signal,\n        reraise: false\n      },\n      async () => {\n        const response = await fetch(`${this.baseUrl}${endpoint}`);\n        if (!response.ok) throw new Error(`HTTP ${response.status}`);\n        return response.json();\n      }\n    );\n\n    try {\n      const data = await fetchWithRetry();\n      clearTimeout(timeoutId);\n      return data;\n    } catch (error) {\n      clearTimeout(timeoutId);\n      if (error instanceof AbortError) {\n        console.log('Request cancelled due to timeout');\n      } else if (error instanceof MaxAttemptsError) {\n        console.log(`Failed after ${error.retryCount} attempts`);\n      }\n      throw error;\n    }\n  }\n}\n```\n\n## API Reference\n\n### Exports\n\n```typescript\n// Main functions\nexport function Retryable(options: RetryOptions): DecoratorFunction;\nexport function withRetry<T extends (...args: any[]) => any>(\n  options: RetryOptions,\n  fn: T\n): T;\n\n// Error classes\nexport class MaxAttemptsError extends Error {\n  code: string;\n  retryCount: number;\n  originalError: Error;\n}\n\nexport class AbortError extends Error {\n  code: string;\n  name: string;\n}\n\n// Enums\nexport enum BackOffPolicy {\n  FixedBackOffPolicy = 'FixedBackOffPolicy',\n  ExponentialBackOffPolicy = 'ExponentialBackOffPolicy'\n}\n\n// Interfaces\nexport interface RetryOptions {\n  maxAttempts: number;\n  backOffPolicy?: BackOffPolicy;\n  backOff?: number;\n  doRetry?: (e: any) => boolean;\n  value?: ErrorConstructor[];\n  exponentialOption?: { maxInterval: number; multiplier: number };\n  reraise?: boolean;\n  signal?: AbortSignal;\n  useJitter?: boolean;\n  jitterType?: 'full' | 'equal' | 'decorrelated';\n}\n\nexport type JitterType = 'full' | 'equal' | 'decorrelated';\n```\n\n## Common Use Cases\n\n### API Rate Limiting\n```typescript\nconst apiCall = withRetry(\n  {\n    maxAttempts: 5,\n    backOff: 1000,\n    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n    doRetry: (e: Error) => e.message.includes('429')\n  },\n  async () => await fetch('/api/data')\n);\n```\n\n### Network Resilience\n```typescript\n@Retryable({\n  maxAttempts: 3,\n  backOff: 2000,\n  value: [TypeError, NetworkError], // Retry only on network errors\n  exponentialOption: { maxInterval: 10000, multiplier: 2 }\n})\nasync fetchFromUnstableService() {\n  // Your code here\n}\n```\n\n### Preventing Thundering Herd in Microservices\n```typescript\n// When multiple service instances fail simultaneously,\n// jitter prevents them all from retrying at the exact same time\n@Retryable({\n  maxAttempts: 5,\n  backOff: 2000,\n  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,\n  exponentialOption: { maxInterval: 30000, multiplier: 2 },\n  useJitter: true,\n  jitterType: 'full'  // Spreads retry attempts across time\n})\nasync callDownstreamService(serviceUrl: string) {\n  const response = await fetch(serviceUrl);\n  if (!response.ok) throw new Error(`Service error: ${response.status}`);\n  return response.json();\n}\n```\n\n### User-Cancellable Operations\n```typescript\nconst controller = new AbortController();\n\n// Show cancel button to user\ndocument.getElementById('cancelBtn').onclick = () => controller.abort();\n\nconst operation = withRetry(\n  { maxAttempts: 10, backOff: 1000, signal: controller.signal },\n  async () => await longRunningOperation()\n);\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}