RefreshTimer.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /**
  2. * RefreshTimer - A utility for executing a callback with configurable retry behavior
  3. *
  4. * This timer executes a callback function and schedules the next execution based on the result:
  5. * - If the callback succeeds (returns true), it schedules the next attempt after a fixed interval
  6. * - If the callback fails (returns false), it uses exponential backoff up to a maximum interval
  7. */
  8. /**
  9. * Configuration options for the RefreshTimer
  10. */
  11. export interface RefreshTimerOptions {
  12. /**
  13. * The callback function to execute
  14. * Should return a Promise that resolves to a boolean indicating success (true) or failure (false)
  15. */
  16. callback: () => Promise<boolean>
  17. /**
  18. * Time in milliseconds to wait before next attempt after success
  19. * @default 50000 (50 seconds)
  20. */
  21. successInterval?: number
  22. /**
  23. * Initial backoff time in milliseconds for the first failure
  24. * @default 1000 (1 second)
  25. */
  26. initialBackoffMs?: number
  27. /**
  28. * Maximum backoff time in milliseconds
  29. * @default 300000 (5 minutes)
  30. */
  31. maxBackoffMs?: number
  32. }
  33. /**
  34. * A timer utility that executes a callback with configurable retry behavior
  35. */
  36. export class RefreshTimer {
  37. private callback: () => Promise<boolean>
  38. private successInterval: number
  39. private initialBackoffMs: number
  40. private maxBackoffMs: number
  41. private currentBackoffMs: number
  42. private attemptCount: number
  43. private timerId: NodeJS.Timeout | null
  44. private isRunning: boolean
  45. /**
  46. * Creates a new RefreshTimer
  47. *
  48. * @param options Configuration options for the timer
  49. */
  50. constructor(options: RefreshTimerOptions) {
  51. this.callback = options.callback
  52. this.successInterval = options.successInterval ?? 50000 // 50 seconds
  53. this.initialBackoffMs = options.initialBackoffMs ?? 1000 // 1 second
  54. this.maxBackoffMs = options.maxBackoffMs ?? 300000 // 5 minutes
  55. this.currentBackoffMs = this.initialBackoffMs
  56. this.attemptCount = 0
  57. this.timerId = null
  58. this.isRunning = false
  59. }
  60. /**
  61. * Starts the timer and executes the callback immediately
  62. */
  63. public start(): void {
  64. if (this.isRunning) {
  65. return
  66. }
  67. this.isRunning = true
  68. // Execute the callback immediately
  69. this.executeCallback()
  70. }
  71. /**
  72. * Stops the timer and cancels any pending execution
  73. */
  74. public stop(): void {
  75. if (!this.isRunning) {
  76. return
  77. }
  78. if (this.timerId) {
  79. clearTimeout(this.timerId)
  80. this.timerId = null
  81. }
  82. this.isRunning = false
  83. }
  84. /**
  85. * Resets the backoff state and attempt count
  86. * Does not affect whether the timer is running
  87. */
  88. public reset(): void {
  89. this.currentBackoffMs = this.initialBackoffMs
  90. this.attemptCount = 0
  91. }
  92. /**
  93. * Schedules the next attempt based on the success/failure of the current attempt
  94. *
  95. * @param wasSuccessful Whether the current attempt was successful
  96. */
  97. private scheduleNextAttempt(wasSuccessful: boolean): void {
  98. if (!this.isRunning) {
  99. return
  100. }
  101. if (wasSuccessful) {
  102. // Reset backoff on success
  103. this.currentBackoffMs = this.initialBackoffMs
  104. this.attemptCount = 0
  105. this.timerId = setTimeout(() => this.executeCallback(), this.successInterval)
  106. } else {
  107. // Increment attempt count
  108. this.attemptCount++
  109. // Calculate backoff time with exponential increase
  110. // Formula: initialBackoff * 2^(attemptCount - 1)
  111. this.currentBackoffMs = Math.min(
  112. this.initialBackoffMs * Math.pow(2, this.attemptCount - 1),
  113. this.maxBackoffMs,
  114. )
  115. this.timerId = setTimeout(() => this.executeCallback(), this.currentBackoffMs)
  116. }
  117. }
  118. /**
  119. * Executes the callback and handles the result
  120. */
  121. private async executeCallback(): Promise<void> {
  122. if (!this.isRunning) {
  123. return
  124. }
  125. try {
  126. const result = await this.callback()
  127. this.scheduleNextAttempt(result)
  128. } catch (_error) {
  129. // Treat errors as failed attempts
  130. this.scheduleNextAttempt(false)
  131. }
  132. }
  133. }