mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

limiter.ts

Language TypeScript1.6 KB55 lines

packages/shared/src/limiter.ts

  1. import { sleep as realSleep } from './retry.js'
  2. /**
  3. * Token bucket rate limiter.
  4. *
  5. * Dependency-free and clock-injectable so it can be tested without real time
  6. * passing, and so it runs unchanged in a Cloudflare Worker.
  7. */
  8. export class TokenBucket {
  9. private tokens: number
  10. private lastRefill: number
  11. constructor(
  12. private readonly capacity: number,
  13. private readonly refillPerSecond: number,
  14. private readonly now: () => number = () => Date.now(),
  15. private readonly sleep: (ms: number) => Promise<void> = realSleep,
  16. ) {
  17. if (capacity <= 0 || refillPerSecond <= 0) {
  18. throw new Error('TokenBucket requires positive capacity and refill rate')
  19. }
  20. this.tokens = capacity
  21. this.lastRefill = now()
  22. }
  23. private refill(): void {
  24. const t = this.now()
  25. const elapsedSec = (t - this.lastRefill) / 1000
  26. if (elapsedSec <= 0) return
  27. this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSecond)
  28. this.lastRefill = t
  29. }
  30. /** Resolves when a token is available. Never rejects. */
  31. async acquire(): Promise<void> {
  32. // Loop rather than compute-once: other callers may drain the bucket while
  33. // this one is sleeping.
  34. for (;;) {
  35. this.refill()
  36. if (this.tokens >= 1) {
  37. this.tokens -= 1
  38. return
  39. }
  40. const deficit = 1 - this.tokens
  41. const waitMs = Math.ceil((deficit / this.refillPerSecond) * 1000)
  42. await this.sleep(Math.max(waitMs, 1))
  43. }
  44. }
  45. /** Test/debug visibility. Not part of the public contract. */
  46. get available(): number {
  47. this.refill()
  48. return this.tokens
  49. }
  50. }