{"version":3,"file":"useAsyncRateLimiter.cjs","names":["useDefaultPacerOptions","AsyncRateLimiter","shallow"],"sources":["../../src/async-rate-limiter/useAsyncRateLimiter.ts"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react'\nimport { AsyncRateLimiter } from '@tanstack/pacer/async-rate-limiter'\nimport { shallow, useSelector } from '@tanstack/react-store'\nimport { useDefaultPacerOptions } from '../provider/PacerProvider'\nimport type { Store } from '@tanstack/react-store'\nimport type { AnyAsyncFunction } from '@tanstack/pacer/types'\nimport type {\n  AsyncRateLimiterOptions,\n  AsyncRateLimiterState,\n} from '@tanstack/pacer/async-rate-limiter'\nimport type { FunctionComponent, ReactNode } from 'react'\n\nexport interface ReactAsyncRateLimiterOptions<\n  TFn extends AnyAsyncFunction,\n  TSelected = {},\n> extends AsyncRateLimiterOptions<TFn> {\n  /**\n   * Optional callback invoked when the component unmounts. Receives the rate limiter instance.\n   * When provided, replaces the default cleanup (abort); use it to call reset(), add logging, etc.\n   */\n  onUnmount?: (rateLimiter: ReactAsyncRateLimiter<TFn, TSelected>) => void\n}\n\nexport interface ReactAsyncRateLimiter<\n  TFn extends AnyAsyncFunction,\n  TSelected = {},\n> extends Omit<AsyncRateLimiter<TFn>, 'store'> {\n  /**\n   * A React HOC (Higher Order Component) that allows you to subscribe to the rate limiter state.\n   *\n   * This is useful for opting into state re-renders for specific parts of the rate limiter state\n   * deep in your component tree without needing to pass a selector to the hook.\n   *\n   * @example\n   * <rateLimiter.Subscribe selector={(state) => ({ rejectionCount: state.rejectionCount, isExecuting: state.isExecuting })}>\n   *   {({ rejectionCount, isExecuting }) => (\n   *     <div>Rejected: {rejectionCount}, {isExecuting ? 'Executing' : 'Idle'}</div>\n   *   )}\n   * </rateLimiter.Subscribe>\n   */\n  Subscribe: <TSelected>(props: {\n    selector: (state: AsyncRateLimiterState<TFn>) => TSelected\n    children: ((state: TSelected) => ReactNode) | ReactNode\n  }) => ReturnType<FunctionComponent>\n  /**\n   * Reactive state that will be updated and re-rendered when the rate limiter state changes\n   *\n   * Use this instead of `rateLimiter.store.state`\n   */\n  readonly state: Readonly<TSelected>\n  /**\n   * @deprecated Use `rateLimiter.state` instead of `rateLimiter.store.state` if you want to read reactive state.\n   * The state on the store object is not reactive, as it has not been wrapped in a `useSelector` hook internally.\n   * Although, you can make the state reactive by using the `useSelector` in your own usage.\n   */\n  readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>\n}\n\n/**\n * A low-level React hook that creates an `AsyncRateLimiter` instance to limit how many times an async function can execute within a time window.\n *\n * This hook is designed to be flexible and state-management agnostic - it simply returns a rate limiter instance that\n * you can integrate with any state management solution (useState, Redux, Zustand, Jotai, etc).\n *\n * Rate limiting allows an async function to execute up to a specified limit within a time window,\n * then blocks subsequent calls until the window passes. This is useful for respecting API rate limits,\n * managing resource constraints, or controlling bursts of async operations.\n *\n * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * The rate limiter supports two types of windows:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n *   towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n *   consistent rate of execution over time.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * ## State Management and Selector\n *\n * The hook uses TanStack Store for reactive state management. You can subscribe to state changes\n * in two ways:\n *\n * **1. Using `rateLimiter.Subscribe` HOC (Recommended for component tree subscriptions)**\n *\n * Use the `Subscribe` HOC to subscribe to state changes deep in your component tree without\n * needing to pass a selector to the hook. This is ideal when you want to subscribe to state\n * in child components.\n *\n * **2. Using the `selector` parameter (For hook-level subscriptions)**\n *\n * The `selector` parameter allows you to specify which state changes will trigger a re-render\n * at the hook level, optimizing performance by preventing unnecessary re-renders when irrelevant\n * state changes occur.\n *\n * **By default, there will be no reactive state subscriptions** and you must opt-in to state\n * tracking by providing a selector function or using the `Subscribe` HOC. This prevents unnecessary\n * re-renders and gives you full control over when your component updates.\n *\n * Available state properties:\n * - `errorCount`: Number of function executions that have resulted in errors\n * - `executionTimes`: Array of timestamps when executions occurred for rate limiting calculations\n * - `isExecuting`: Whether the rate-limited function is currently executing asynchronously\n * - `lastResult`: The result from the most recent successful function execution\n * - `rejectionCount`: Number of function executions that have been rejected due to rate limiting\n * - `settleCount`: Number of function executions that have completed (success or error)\n * - `successCount`: Number of function executions that have completed successfully\n *\n * ## Unmount behavior\n *\n * By default, the hook aborts any in-flight execution when the component unmounts.\n * Abort only cancels underlying operations (e.g. fetch) when the abort signal from `getAbortSignal()` is passed to them.\n * Use the `onUnmount` option to customize this.\n *\n * @example\n * ```tsx\n * // Default behavior - no reactive state subscriptions\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data; // Return value is preserved\n *   },\n *   { limit: 5, window: 1000 } // 5 calls per second\n * );\n *\n * // Subscribe to state changes deep in component tree using Subscribe HOC\n * <asyncRateLimiter.Subscribe selector={(state) => ({ rejectionCount: state.rejectionCount, isExecuting: state.isExecuting })}>\n *   {({ rejectionCount, isExecuting }) => (\n *     <div>Rejected: {rejectionCount}, {isExecuting ? 'Executing' : 'Idle'}</div>\n *   )}\n * </asyncRateLimiter.Subscribe>\n *\n * // Opt-in to re-render when execution state changes at hook level (optimized for loading indicators)\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data;\n *   },\n *   { limit: 5, window: 1000 },\n *   (state) => ({ isExecuting: state.isExecuting })\n * );\n *\n * // Opt-in to re-render when results are available (optimized for data display)\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data;\n *   },\n *   { limit: 5, window: 1000 },\n *   (state) => ({\n *     lastResult: state.lastResult,\n *     successCount: state.successCount\n *   })\n * );\n *\n * // Opt-in to re-render when error/rejection state changes (optimized for error handling)\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data;\n *   },\n *   {\n *     limit: 5,\n *     window: 1000,\n *     onError: (error) => console.error('API call failed:', error),\n *     onReject: (rateLimiter) => console.log('Rate limit exceeded')\n *   },\n *   (state) => ({\n *     errorCount: state.errorCount,\n *     rejectionCount: state.rejectionCount\n *   })\n * );\n *\n * // Opt-in to re-render when execution metrics change (optimized for stats display)\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data;\n *   },\n *   { limit: 5, window: 1000 },\n *   (state) => ({\n *     successCount: state.successCount,\n *     errorCount: state.errorCount,\n *     settleCount: state.settleCount,\n *     rejectionCount: state.rejectionCount\n *   })\n * );\n *\n * // Opt-in to re-render when execution times change (optimized for window calculations)\n * const asyncRateLimiter = useAsyncRateLimiter(\n *   async (id: string) => {\n *     const data = await api.fetchData(id);\n *     return data;\n *   },\n *   { limit: 5, window: 1000 },\n *   (state) => ({ executionTimes: state.executionTimes })\n * );\n *\n * // With state management and return value\n * const [data, setData] = useState(null);\n * const { maybeExecute, state } = useAsyncRateLimiter(\n *   async (query) => {\n *     const result = await searchAPI(query);\n *     setData(result);\n *     return result; // Return value can be used by the caller\n *   },\n *   {\n *     limit: 10,\n *     window: 60000, // 10 calls per minute\n *     onReject: (rateLimiter) => {\n *       console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n *     },\n *     onError: (error) => {\n *       console.error('API call failed:', error);\n *     }\n *   }\n * );\n *\n * // Access the selected state (will be empty object {} unless selector provided)\n * const { isExecuting, lastResult, rejectionCount } = state;\n * ```\n */\nexport function useAsyncRateLimiter<\n  TFn extends AnyAsyncFunction,\n  TSelected = {},\n>(\n  fn: TFn,\n  options: ReactAsyncRateLimiterOptions<TFn, TSelected>,\n  selector: (state: AsyncRateLimiterState<TFn>) => TSelected = () =>\n    ({}) as TSelected,\n): ReactAsyncRateLimiter<TFn, TSelected> {\n  const mergedOptions = {\n    ...useDefaultPacerOptions().asyncRateLimiter,\n    ...options,\n  } as ReactAsyncRateLimiterOptions<TFn, TSelected>\n  const [asyncRateLimiter] = useState(() => {\n    const asyncRateLimiterInstance = new AsyncRateLimiter<TFn>(\n      fn,\n      mergedOptions,\n    ) as unknown as ReactAsyncRateLimiter<TFn, TSelected>\n\n    /* eslint-disable-next-line @eslint-react/component-hook-factories -- Subscribe attached once in useState lazy init; stable per instance */\n    asyncRateLimiterInstance.Subscribe = function Subscribe<TSelected>(props: {\n      selector: (state: AsyncRateLimiterState<TFn>) => TSelected\n      children: ((state: TSelected) => ReactNode) | ReactNode\n    }) {\n      const selected = useSelector(\n        asyncRateLimiterInstance.store,\n        props.selector,\n        { compare: shallow },\n      )\n\n      return typeof props.children === 'function'\n        ? props.children(selected)\n        : props.children\n    }\n\n    return asyncRateLimiterInstance\n  })\n\n  asyncRateLimiter.fn = fn\n  asyncRateLimiter.setOptions(mergedOptions)\n\n  /* eslint-disable react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps, react-compiler/react-compiler -- unmount cleanup only; empty deps keep teardown stable */\n  useEffect(() => {\n    return () => {\n      if (mergedOptions.onUnmount) {\n        mergedOptions.onUnmount(asyncRateLimiter)\n      } else {\n        asyncRateLimiter.abort()\n      }\n    }\n  }, [])\n  /* eslint-enable react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps, react-compiler/react-compiler */\n\n  const state = useSelector(asyncRateLimiter.store, selector, {\n    compare: shallow,\n  })\n\n  return useMemo(\n    () =>\n      ({\n        ...asyncRateLimiter,\n        state,\n      }) as ReactAsyncRateLimiter<TFn, TSelected>, // omit `store` in favor of `state`\n    [asyncRateLimiter, state],\n  )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsOA,SAAgB,oBAId,IACA,SACA,kBACG,EAAE,GACkC;CACvC,MAAM,gBAAgB;EACpB,GAAGA,8CAAwB,CAAC;EAC5B,GAAG;EACJ;CACD,MAAM,CAAC,8CAAmC;EACxC,MAAM,2BAA2B,IAAIC,oDACnC,IACA,cACD;AAGD,2BAAyB,YAAY,SAAS,UAAqB,OAGhE;GACD,MAAM,kDACJ,yBAAyB,OACzB,MAAM,UACN,EAAE,SAASC,+BAAS,CACrB;AAED,UAAO,OAAO,MAAM,aAAa,aAC7B,MAAM,SAAS,SAAS,GACxB,MAAM;;AAGZ,SAAO;GACP;AAEF,kBAAiB,KAAK;AACtB,kBAAiB,WAAW,cAAc;AAG1C,4BAAgB;AACd,eAAa;AACX,OAAI,cAAc,UAChB,eAAc,UAAU,iBAAiB;OAEzC,kBAAiB,OAAO;;IAG3B,EAAE,CAAC;CAGN,MAAM,+CAAoB,iBAAiB,OAAO,UAAU,EAC1D,SAASA,+BACV,CAAC;AAEF,kCAEK;EACC,GAAG;EACH;EACD,GACH,CAAC,kBAAkB,MAAM,CAC1B"}