{"version":3,"file":"limiter.mjs","names":[],"sources":["../../src/utils/limiter.ts"],"sourcesContent":["// This file is duplicated verbatim in @storyblok/api-client and\n// @storyblok/management-api-client. The two copies must stay identical; apply\n// any change to both. It is intentionally not extracted into a shared package\n// so the two independently published clients keep no shared runtime dependency.\n\nimport { createThrottle, type Throttle } from \"./throttle\";\n\n/**\n * Describes the request a limiter is being asked to admit.\n */\nexport interface RateLimitContext {\n  /** Request path, e.g. `/v2/cdn/stories/home`. */\n  path: string;\n  /** Query parameters of the request, less the access token. */\n  query: Record<string, unknown>;\n  /**\n   * Name of the quota this request draws from. The Content API client buckets\n   * per rate-limit tier; the Management API client uses a single bucket.\n   */\n  bucket: string;\n  /**\n   * Requests per second the client would allow for this bucket on its own.\n   *\n   * A custom limiter coordinating several instances against one token divides\n   * this by the number of instances rather than hard-coding a limit that would\n   * have to be the lowest tier's to stay safe.\n   */\n  limit: number;\n}\n\n/**\n * Admission control for outgoing requests.\n *\n * Implement this to coordinate a fleet of clients sharing one token, for\n * example against Redis. The in-memory default applies per-second windows local\n * to the instance.\n *\n * The three hooks are called once per HTTP request, retries included, and are\n * handed the same context object so a limiter can pair a release to its\n * acquire. A rejection from `acquire` fails the request; one from either\n * reporting hook is swallowed, so a blip in shared storage cannot turn a served\n * request into an error.\n */\nexport interface RateLimiter {\n  /**\n   * Resolves when the request may start. Nothing times this out, so a limiter\n   * that can stall must impose its own deadline.\n   */\n  acquire: (context: RateLimitContext) => Promise<void>;\n  /**\n   * Reports a response, including one a retry went on to replace — a limiter\n   * that only saw a request's last response would miss most of the throttling\n   * it is meant to react to.\n   *\n   * The response is a clone, so reading its body is safe.\n   */\n  recordResponse?: (context: RateLimitContext, response: Response) => void | Promise<void>;\n  /** Reports that the request settled, whether or not it succeeded. */\n  release?: (context: RateLimitContext) => void | Promise<void>;\n}\n\n/**\n * Tuning for the additive-increase/multiplicative-decrease adaptation applied\n * by the default limiter.\n */\nexport interface AdaptiveConfig {\n  /**\n   * Fraction the effective limit drops to when a request is throttled. A value\n   * outside `(0, 1)` would make a back-off an increase, and falls back to the\n   * default.\n   * @default 0.5\n   */\n  decreaseFactor?: number;\n  /**\n   * Requests per second added back per `recoveryIntervalMs` of sustained\n   * success. Defaults to a twenty-fifth of the bucket's ceiling, at least 1, so\n   * that recovery takes about as long on a 50/s tier as on a 6/s one. A ceiling\n   * above eight times the bucket's limit counts as eight times it.\n   */\n  increaseStep?: number;\n  /**\n   * How long the limit must hold without being throttled before it recovers.\n   * @default 1000\n   */\n  recoveryIntervalMs?: number;\n  /**\n   * Floor the effective limit never drops below. Values below 1 are raised to 1.\n   * @default 1\n   */\n  minRequestsPerSecond?: number;\n  /**\n   * Shortest spacing between two decreases of the same bucket. A burst of\n   * responses throttled by one overloaded window describes a single event, so\n   * without this the limit would collapse to the floor on the first burst.\n   * @default 1000\n   */\n  decreaseCooldownMs?: number;\n}\n\n/**\n * Lets a bucket's ceiling rise above its configured limit while an edge cache\n * is absorbing the traffic, instead of pinning it to the origin's quota.\n *\n * Both fields are required, so the mechanism is inert wherever it is not\n * configured — an API with no cache in front of it never opts in.\n */\nexport interface CacheAwareConfig {\n  /**\n   * Highest rate the cache is assumed able to serve; the ceiling never passes it.\n   *\n   * It is an outer bound, not the operating one: twenty times a bucket's own\n   * limit binds first on every bucket under a twentieth of this.\n   */\n  cachedRequestsPerSecond: number;\n  /**\n   * Whether the response was served from cache without reaching the origin.\n   *\n   * Returns `undefined` when the response carries no such signal, which keeps\n   * the bucket at its configured limit. A runtime that hides the header — a\n   * browser, for one — therefore never leaves that limit.\n   */\n  detectCacheHit: (response: Response) => boolean | undefined;\n}\n\n/**\n * What a bucket's pacing looks like after a response has been accounted for.\n */\nexport interface RateLimitStatus {\n  /** Quota the request drew from, as `context.bucket` named it. */\n  bucket: string;\n  /** Requests per second the bucket is admitting right now. */\n  requestsPerSecond: number;\n  /** Highest rate the bucket may recover to. */\n  ceiling: number;\n  /** Rate the bucket would be paced at with no adaptation at all. */\n  configuredLimit: number;\n  /**\n   * Share of the observed responses the cache served, between 0 and 1.\n   *\n   * `undefined` while the bucket has no cache measurement to report: cache\n   * awareness is off, or no response has carried a readable cache status.\n   */\n  cacheHitShare: number | undefined;\n}\n\n/** Lowest rate that still paces anything. */\nconst MIN_REQUESTS_PER_SECOND = 1;\n\n/**\n * Makes a rate safe to hand to a per-second window.\n *\n * A window reads a rate of zero as \"no limit\", so a rate that floors, divides\n * or rounds its way down to zero turns pacing off altogether — the opposite of\n * what every caller computing one intends. `NaN` does the same by propagating\n * through the comparisons that would otherwise bound it. Every rate derived\n * from arithmetic, whether from a configured value, a response header or a\n * measured share, passes through here before it reaches a window.\n */\nexport function clampRequestsPerSecond(rate: number, minimum = MIN_REQUESTS_PER_SECOND): number {\n  return Number.isFinite(rate) ? Math.max(minimum, rate) : minimum;\n}\n\nconst ADAPTIVE_DEFAULTS: Required<Omit<AdaptiveConfig, \"increaseStep\">> = {\n  decreaseFactor: 0.5,\n  recoveryIntervalMs: 1000,\n  minRequestsPerSecond: MIN_REQUESTS_PER_SECOND,\n  decreaseCooldownMs: 1000,\n};\n\n/** A bucket recovers its whole rate in approximately this many intervals. */\nconst RECOVERY_INTERVALS = 25;\n\n/**\n * Highest ceiling, as a multiple of the configured limit, the recovery step\n * scales with. A steeper climb opens more connections per second, and their\n * setup stalls short workloads such as a build by seconds; above this multiple\n * the ceiling is only reached more slowly, not more steeply.\n */\nconst CLIMB_STEP_MULTIPLE = 8;\n\n/**\n * Responses a bucket's cache-hit share is measured over. The ceiling only\n * leaves the configured limit once this many have been observed, and a run of\n * origin-served responses flushes the window in the same number again.\n */\nconst CACHE_SAMPLE_WINDOW = 50;\n\n/**\n * How unlikely a run of origin-served responses has to be, under the hit share\n * the bucket has been measuring, before it is read as the traffic going cold\n * rather than as the ordinary clustering of a mixed workload.\n */\nconst COLD_RUN_IMPROBABILITY = 1e-5;\n\n/** Runs shorter than the minimum are never a cold run; a mostly-missing bucket never waits longer than the maximum. */\nconst COLD_RUN_BOUNDS = { min: 3, max: 25 } as const;\n\n/**\n * Consecutive origin-served responses that put a bucket back on its configured\n * limit at once, without waiting for the window's average to follow.\n *\n * A working set going cold shows up as an unbroken run of misses well before\n * the average moves, and the misses lead the first 429. But a run is only\n * evidence of that if the measured traffic would not produce one on its own: at\n * a half-cached workload runs of ten arrive on their own often enough to cost\n * real throughput, while a workload the cache serves almost entirely should be\n * believed after three or four. So the threshold is derived from the miss share\n * rather than fixed — a run long enough to be surprising at the rate misses are\n * actually arriving.\n *\n * A single hit lifts the clamp again, and the window is kept, so a brief cold\n * patch costs the climb back and not the measurement.\n */\nfunction coldRunLengthFor(missShare: number): number {\n  if (missShare <= 0 || missShare >= 1) {\n    return COLD_RUN_BOUNDS.min;\n  }\n  const surprising = Math.log(COLD_RUN_IMPROBABILITY) / Math.log(missShare);\n  return Math.min(COLD_RUN_BOUNDS.max, Math.max(COLD_RUN_BOUNDS.min, Math.ceil(surprising)));\n}\n\n/**\n * How long a bucket may go unobserved before its cache measurement is discarded.\n *\n * A window carries no time of its own, so without this a bucket that ran hot,\n * went quiet for an hour and woke up would admit its whole raised ceiling at\n * once, on the strength of a measurement describing traffic that is long over\n * and against keys that may well have expired. Forgetting costs the client the\n * climb back — a window of responses at the configured limit — which is the\n * same price a cold start pays.\n */\nconst CACHE_WINDOW_MAX_IDLE_MS = 60_000;\n\n/**\n * How far above its configured limit a bucket may be paced, however cached its\n * traffic looks.\n *\n * When a working set goes cold, everything already admitted is in flight before\n * the first response can report it, so the requests that overshoot the origin\n * are roughly the ceiling times one round trip — a cost no feedback can react\n * its way out of. Bounding the multiple bounds that burst for every tier at\n * once, and costs little: the measured gain flattens well before the ceiling\n * runs out of room.\n */\nconst MAX_CEILING_MULTIPLE = 20;\n\n/**\n * Recent responses, across all buckets, that decide whether the traffic is\n * reaching the origin. A publish rotates the content version and turns every key\n * cold at once, so the first response of a fresh client already tells.\n */\nconst ORIGIN_SIGNAL_WINDOW = 20;\n\n/** 503 is excluded: it reports an upstream problem that backing off would not address. */\nconst THROTTLED_STATUS = 429;\n\nexport interface DefaultRateLimiterOptions {\n  /** AIMD adaptation. `false` pins every bucket to its base limit. @default true */\n  adaptive?: boolean | AdaptiveConfig;\n  /**\n   * Lowers a bucket's ceiling to the quota advertised by a response header.\n   * Returns `undefined` when the response carries no applicable quota.\n   */\n  parseServerLimit?: (response: Response) => number | undefined;\n  /**\n   * Raises a bucket's ceiling in proportion to the share of its responses the\n   * cache serves. Omitted, the ceiling never passes the configured limit.\n   *\n   * Ignored when `adaptive` is `false`, which pins every bucket outright.\n   */\n  cacheAware?: CacheAwareConfig;\n  /**\n   * Called whenever a response changes how a bucket is being paced, so a\n   * caller can see the rate it is running at without inferring it from timings.\n   *\n   * It runs on the response path, so it should return quickly. A failure is\n   * swallowed: observing the limiter must not turn a served request into an\n   * error.\n   */\n  onRateLimitChange?: (status: RateLimitStatus) => void;\n}\n\ninterface Bucket {\n  throttle: Throttle;\n  /** Rate the client asked for. */\n  configuredLimit: number;\n  /** Most recent quota a response advertised, if any. */\n  serverLimit?: number;\n  lastDecreaseAt: number;\n  lastIncreaseAt: number;\n  /** Ring buffer of the last `CACHE_SAMPLE_WINDOW` cache observations. */\n  cacheSamples: boolean[];\n  cacheSampleCursor: number;\n  cacheHitCount: number;\n  /** Origin-served responses since the last cache hit. */\n  consecutiveMisses: number;\n  /** When the window last took a sample, for ageing it out while the bucket is idle. */\n  lastCacheObservationAt: number;\n  /** Earliest start of the next request while the traffic is origin-bound. */\n  nextSpacedStartAt: number;\n  /** Pacing last reported to `onRateLimitChange`, so unchanged pacing is not reported twice. */\n  reportedStatus?: Omit<RateLimitStatus, \"bucket\">;\n}\n\n/**\n * Rejects a non-finite tuning value before it reaches `Math.max`, which\n * propagates `NaN` rather than bounding it.\n */\nconst finiteOr = (value: number | undefined, fallback: number): number =>\n  value !== undefined && Number.isFinite(value) ? value : fallback;\n\n/**\n * Creates the in-memory limiter used unless the caller supplies its own.\n *\n * Each bucket gets a per-second window sized by `context.limit`, shrinking\n * multiplicatively on a throttled response and recovering additively while none\n * is. It never drops below the configured floor, and never rises above\n * `context.limit` unless `cacheAware` is configured and a cache is observably\n * absorbing the bucket's traffic, in which case it may recover to a multiple of\n * it.\n */\nexport function createDefaultRateLimiter(options: DefaultRateLimiterOptions = {}): RateLimiter {\n  const { adaptive = true, parseServerLimit, onRateLimitChange } = options;\n  const configured = typeof adaptive === \"object\" ? adaptive : {};\n  const requestedDecrease = configured.decreaseFactor ?? ADAPTIVE_DEFAULTS.decreaseFactor;\n  const adaptiveConfig = {\n    ...ADAPTIVE_DEFAULTS,\n    ...configured,\n    decreaseFactor:\n      requestedDecrease > 0 && requestedDecrease < 1\n        ? requestedDecrease\n        : ADAPTIVE_DEFAULTS.decreaseFactor,\n    increaseStep:\n      configured.increaseStep === undefined\n        ? undefined\n        : Math.max(0, finiteOr(configured.increaseStep, 0)),\n    recoveryIntervalMs: finiteOr(\n      configured.recoveryIntervalMs,\n      ADAPTIVE_DEFAULTS.recoveryIntervalMs,\n    ),\n    decreaseCooldownMs: finiteOr(\n      configured.decreaseCooldownMs,\n      ADAPTIVE_DEFAULTS.decreaseCooldownMs,\n    ),\n    minRequestsPerSecond: clampRequestsPerSecond(\n      configured.minRequestsPerSecond ?? MIN_REQUESTS_PER_SECOND,\n    ),\n  };\n\n  const stepFor = (bucket: Bucket, ceiling: number) =>\n    adaptiveConfig.increaseStep ??\n    Math.max(\n      1,\n      Math.round(\n        Math.min(ceiling, bucket.configuredLimit * CLIMB_STEP_MULTIPLE) / RECOVERY_INTERVALS,\n      ),\n    );\n  const adaptationEnabled = adaptive !== false;\n  // `adaptive: false` means \"pin every bucket to its base limit\", so a\n  // discovered ceiling has no business overriding it.\n  const cacheAware = adaptationEnabled ? options.cacheAware : undefined;\n  const buckets = new Map<string, Bucket>();\n  /** Whether each of the last `ORIGIN_SIGNAL_WINDOW` responses reached the origin. */\n  const originSignals: boolean[] = [];\n\n  const recordOriginSignal = (reachedOrigin: boolean) => {\n    originSignals.push(reachedOrigin);\n    if (originSignals.length > ORIGIN_SIGNAL_WINDOW) {\n      originSignals.shift();\n    }\n  };\n\n  /**\n   * Only origin-bound traffic gets throttled for bursting: a full window\n   * released at once arrives packed into less than a second, and the origin\n   * answers part of it with 429. Cached traffic keeps the instant window.\n   */\n  const isOriginBound = (): boolean =>\n    originSignals.length > 0 && originSignals.filter(Boolean).length * 2 >= originSignals.length;\n\n  /** Resolves at the bucket's next evenly spaced start, `1000 / rate` ms after the previous one. */\n  const awaitSpacedStart = (bucket: Bucket): Promise<void> => {\n    const now = Date.now();\n    const start = Math.max(now, bucket.nextSpacedStartAt);\n    bucket.nextSpacedStartAt = start + 1000 / bucket.throttle.getLimit();\n    return start > now\n      ? new Promise((resolve) => setTimeout(resolve, start - now))\n      : Promise.resolve();\n  };\n\n  /** Share of the bucket's observed responses the cache served, if it has any. */\n  const hitShareOf = (bucket: Bucket): number | undefined =>\n    cacheAware && bucket.cacheSamples.length > 0\n      ? bucket.cacheHitCount / bucket.cacheSamples.length\n      : undefined;\n\n  /**\n   * Ceiling the effective limit recovers towards; never exceeded.\n   *\n   * Only the share of requests that misses the cache reaches the origin, so a\n   * bucket whose responses are `hitShare` cached can run at\n   * `configuredLimit / (1 - hitShare)` and still put no more than\n   * `configuredLimit` requests per second on the origin. With no observations,\n   * or none of them hits, this is the configured limit exactly.\n   */\n  const ceilingOf = (bucket: Bucket): number => {\n    let ceiling = bucket.configuredLimit;\n    const hitShare = hitShareOf(bucket);\n\n    if (cacheAware && hitShare !== undefined && bucket.cacheSamples.length >= CACHE_SAMPLE_WINDOW) {\n      const missShare = 1 - hitShare;\n      // The headroom is what cache hits add, so a misconfigured cached rate\n      // must not pace the bucket below the origin quota it asked for.\n      const headroom = clampRequestsPerSecond(\n        Math.min(cacheAware.cachedRequestsPerSecond, bucket.configuredLimit * MAX_CEILING_MULTIPLE),\n        bucket.configuredLimit,\n      );\n      if (bucket.consecutiveMisses < coldRunLengthFor(missShare)) {\n        ceiling =\n          missShare <= 0\n            ? headroom\n            : Math.min(headroom, Math.floor(bucket.configuredLimit / missShare));\n      }\n    }\n\n    // A quota the API advertises wins over a discovered one, including over the\n    // configured limit. Today the Content API advertises no per-second quota,\n    // so this never binds; were it to start advertising one, it would cap the\n    // discovered ceiling and the mechanism would go quiet rather than overrun\n    // a rate the API just named.\n    return Math.min(ceiling, bucket.serverLimit ?? Number.POSITIVE_INFINITY);\n  };\n\n  /**\n   * Discards a cache measurement the bucket has stopped renewing, so a raised\n   * ceiling cannot outlive the traffic that justified it.\n   */\n  const forgetIdleCacheWindow = (bucket: Bucket, now: number) => {\n    if (\n      bucket.cacheSamples.length === 0 ||\n      now - bucket.lastCacheObservationAt < CACHE_WINDOW_MAX_IDLE_MS\n    ) {\n      return;\n    }\n\n    bucket.cacheSamples = [];\n    bucket.cacheSampleCursor = 0;\n    bucket.cacheHitCount = 0;\n    bucket.consecutiveMisses = 0;\n  };\n\n  /** Brings a limit back under a ceiling that has just fallen. */\n  const clampToCeiling = (bucket: Bucket) => {\n    const ceiling = ceilingOf(bucket);\n    if (bucket.throttle.getLimit() > ceiling) {\n      bucket.throttle.setLimit(ceiling);\n    }\n  };\n\n  const observeCacheStatus = (bucket: Bucket, response: Response, now: number) => {\n    let hit: boolean | undefined;\n    try {\n      hit = cacheAware?.detectCacheHit(response);\n    } catch {\n      // A detector that throws must not also cost the response its back-off.\n      return;\n    }\n    if (hit === undefined) {\n      return;\n    }\n    recordOriginSignal(!hit);\n\n    bucket.lastCacheObservationAt = now;\n\n    if (bucket.cacheSamples.length < CACHE_SAMPLE_WINDOW) {\n      bucket.cacheSamples.push(hit);\n    } else {\n      if (bucket.cacheSamples[bucket.cacheSampleCursor]) {\n        bucket.cacheHitCount--;\n      }\n      bucket.cacheSamples[bucket.cacheSampleCursor] = hit;\n      bucket.cacheSampleCursor = (bucket.cacheSampleCursor + 1) % CACHE_SAMPLE_WINDOW;\n    }\n\n    if (hit) {\n      bucket.cacheHitCount++;\n    }\n    bucket.consecutiveMisses = hit ? 0 : bucket.consecutiveMisses + 1;\n  };\n\n  const getBucket = (context: RateLimitContext): Bucket => {\n    const existing = buckets.get(context.bucket);\n    if (existing) {\n      return existing;\n    }\n\n    const bucket: Bucket = {\n      throttle: createThrottle(context.limit),\n      configuredLimit: context.limit,\n      lastDecreaseAt: Number.NEGATIVE_INFINITY,\n      lastIncreaseAt: Number.NEGATIVE_INFINITY,\n      cacheSamples: [],\n      cacheSampleCursor: 0,\n      cacheHitCount: 0,\n      consecutiveMisses: 0,\n      lastCacheObservationAt: Number.NEGATIVE_INFINITY,\n      nextSpacedStartAt: Number.NEGATIVE_INFINITY,\n    };\n    buckets.set(context.bucket, bucket);\n    return bucket;\n  };\n\n  const reportStatus = (bucket: Bucket, context: RateLimitContext) => {\n    if (!onRateLimitChange) {\n      return;\n    }\n\n    const status: Omit<RateLimitStatus, \"bucket\"> = {\n      requestsPerSecond: bucket.throttle.getLimit(),\n      ceiling: ceilingOf(bucket),\n      configuredLimit: bucket.configuredLimit,\n      cacheHitShare: hitShareOf(bucket),\n    };\n    const previous = bucket.reportedStatus;\n    if (\n      previous &&\n      previous.requestsPerSecond === status.requestsPerSecond &&\n      previous.ceiling === status.ceiling &&\n      previous.configuredLimit === status.configuredLimit &&\n      previous.cacheHitShare === status.cacheHitShare\n    ) {\n      return;\n    }\n\n    bucket.reportedStatus = status;\n    try {\n      onRateLimitChange({ bucket: context.bucket, ...status });\n    } catch {\n      // Observing the limiter must never turn a served request into an error.\n    }\n  };\n\n  const decrease = (bucket: Bucket, now: number) => {\n    if (now - bucket.lastDecreaseAt < adaptiveConfig.decreaseCooldownMs) {\n      return;\n    }\n\n    const current = bucket.throttle.getLimit();\n    const next = Math.min(\n      ceilingOf(bucket),\n      Math.max(\n        adaptiveConfig.minRequestsPerSecond,\n        Math.floor(current * adaptiveConfig.decreaseFactor),\n      ),\n    );\n    bucket.lastDecreaseAt = now;\n    // Recovery is measured from the decrease, so a limit that was just cut\n    // holds for a full interval before it starts climbing back.\n    bucket.lastIncreaseAt = now;\n    bucket.throttle.setLimit(next);\n  };\n\n  const increase = (bucket: Bucket, now: number) => {\n    const ceiling = ceilingOf(bucket);\n    const current = bucket.throttle.getLimit();\n    if (current >= ceiling) {\n      return;\n    }\n    if (now - bucket.lastIncreaseAt < adaptiveConfig.recoveryIntervalMs) {\n      return;\n    }\n\n    bucket.lastIncreaseAt = now;\n    bucket.throttle.setLimit(Math.min(ceiling, current + stepFor(bucket, ceiling)));\n  };\n\n  const applyServerLimit = (bucket: Bucket, response: Response) => {\n    let serverLimit: number | undefined;\n    try {\n      serverLimit = parseServerLimit?.(response);\n    } catch {\n      // A parser that throws must not also cost the response its back-off.\n      return;\n    }\n    if (serverLimit === undefined) {\n      return;\n    }\n\n    // The latest response's quota replaces the previous one. Ratcheting it down\n    // instead would pin the client to the lowest quota it ever saw.\n    bucket.serverLimit = clampRequestsPerSecond(serverLimit, adaptiveConfig.minRequestsPerSecond);\n    if (!adaptationEnabled) {\n      bucket.throttle.setLimit(ceilingOf(bucket));\n      return;\n    }\n    clampToCeiling(bucket);\n  };\n\n  return {\n    acquire: async (context) => {\n      const bucket = getBucket(context);\n      if (cacheAware) {\n        // A bucket admits before it can be told anything, so staleness has to\n        // be checked on the way in rather than on the next response.\n        forgetIdleCacheWindow(bucket, Date.now());\n        clampToCeiling(bucket);\n        if (isOriginBound()) {\n          await awaitSpacedStart(bucket);\n        }\n      }\n      return bucket.throttle.acquire();\n    },\n    recordResponse: (context, response) => {\n      const bucket = getBucket(context);\n      applyServerLimit(bucket, response);\n\n      if (!adaptationEnabled) {\n        reportStatus(bucket, context);\n        return;\n      }\n\n      const now = Date.now();\n      forgetIdleCacheWindow(bucket, now);\n      observeCacheStatus(bucket, response, now);\n      // A ceiling that just fell — traffic turning cold — takes effect at once,\n      // rather than waiting for a 429 to bring the limit back down.\n      clampToCeiling(bucket);\n\n      if (response.status === THROTTLED_STATUS) {\n        decrease(bucket, now);\n      } else {\n        // Any answer that is not a refusal counts as the quota holding, a 404\n        // among them: recovering only on 2xx would strand a client whose\n        // workload legitimately produces other statuses.\n        increase(bucket, now);\n      }\n      reportStatus(bucket, context);\n    },\n  };\n}\n\n/** A limiter that admits every request immediately. */\nexport function createPassthroughRateLimiter(): RateLimiter {\n  return { acquire: () => Promise.resolve() };\n}\n"],"mappings":"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"}