{"version":3,"file":"throttle.cjs","names":[],"sources":["../../src/utils/throttle.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\nexport interface Throttle {\n  /** Runs `fn` once a per-second slot is available and returns its result. */\n  execute: <T>(fn: () => Promise<T>) => Promise<T>;\n  /** Adjusts the per-second limit applied to subsequent windows. */\n  setLimit: (limit: number) => void;\n}\n\n/**\n * Creates a per-second rate limiter. At most `limit` calls may start within any\n * rolling one-second window; once the window is full, further calls wait until\n * the oldest call in it ages out. This matches how the Storyblok API enforces\n * its limits: a fixed number of requests per one-second window, not a cap on\n * the number of simultaneous requests.\n *\n * The limiter holds only a list of recent start timestamps, pruned against\n * `Date.now()` on every attempt, and every wait resolves the promise the caller\n * already awaits. It therefore keeps no counter that a scheduled callback must\n * decrement, and schedules no timer that outlives the awaited call. That is a\n * hard requirement on runtimes which suspend between requests and drop pending\n * timers (for example Cloudflare Workers): a shared limiter that released its\n * slots from a detached timer would leak its in-flight count across requests\n * until it deadlocked.\n *\n * A non-positive or non-finite `limit` disables throttling and lets every call\n * through.\n */\nexport function createThrottle(initialLimit: number): Throttle {\n  const intervalMs = 1000;\n  let limit = initialLimit;\n  // Start timestamps of the calls currently inside the rolling window.\n  const starts: number[] = [];\n\n  const acquire = (): Promise<void> => {\n    if (!Number.isFinite(limit) || limit <= 0) {\n      return Promise.resolve();\n    }\n\n    return new Promise<void>((resolve) => {\n      const attempt = () => {\n        // The limit can drop to a non-positive or non-finite value (via\n        // setLimit) while this call is waiting. Re-check the disabled guard so\n        // parked callers resolve instead of rescheduling forever.\n        if (!Number.isFinite(limit) || limit <= 0) {\n          resolve();\n          return;\n        }\n\n        const now = Date.now();\n        while (starts.length > 0 && starts[0] <= now - intervalMs) {\n          starts.shift();\n        }\n\n        if (starts.length < limit) {\n          starts.push(now);\n          resolve();\n          return;\n        }\n\n        // Window is full: retry once the oldest call ages out. The timer only\n        // resolves the promise the caller awaits, so it is never orphaned.\n        const wait = starts[0] + intervalMs - now;\n        setTimeout(attempt, wait > 0 ? wait : 0);\n      };\n\n      attempt();\n    });\n  };\n\n  return {\n    execute: <T>(fn: () => Promise<T>): Promise<T> => acquire().then(fn),\n    setLimit: (n: number) => {\n      limit = n;\n    },\n  };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,eAAe,cAAgC;CAC7D,MAAM,aAAa;CACnB,IAAI,QAAQ;CAEZ,MAAM,SAAmB,EAAE;CAE3B,MAAM,gBAA+B;AACnC,MAAI,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EACtC,QAAO,QAAQ,SAAS;AAG1B,SAAO,IAAI,SAAe,YAAY;GACpC,MAAM,gBAAgB;AAIpB,QAAI,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,GAAG;AACzC,cAAS;AACT;;IAGF,MAAM,MAAM,KAAK,KAAK;AACtB,WAAO,OAAO,SAAS,KAAK,OAAO,MAAM,MAAM,WAC7C,QAAO,OAAO;AAGhB,QAAI,OAAO,SAAS,OAAO;AACzB,YAAO,KAAK,IAAI;AAChB,cAAS;AACT;;IAKF,MAAM,OAAO,OAAO,KAAK,aAAa;AACtC,eAAW,SAAS,OAAO,IAAI,OAAO,EAAE;;AAG1C,YAAS;IACT;;AAGJ,QAAO;EACL,UAAa,OAAqC,SAAS,CAAC,KAAK,GAAG;EACpE,WAAW,MAAc;AACvB,WAAQ;;EAEX"}