{"version":3,"file":"debounce.cjs","names":[],"sources":["../src/debounce.ts"],"sourcesContent":["import type { StrictFunction } from \"./internal/types/StrictFunction\";\n\ntype Debouncer<F extends StrictFunction, IsNullable extends boolean = true> = {\n  /**\n   * Invoke the debounced function.\n   *\n   * @param args - Same as the args for the debounced function.\n   * @returns The last computed value of the debounced function with the\n   * latest args provided to it. If `timing` does not include `leading` then the\n   * the function would return `undefined` until the first cool-down period is\n   * over, otherwise the function would always return the return type of the\n   * debounced function.\n   */\n  readonly call: (\n    ...args: Parameters<F>\n  ) => ReturnType<F> | (true extends IsNullable ? undefined : never);\n\n  /**\n   * Cancels any debounced functions without calling them, effectively resetting\n   * the debouncer to the same state it is when initially created.\n   */\n  readonly cancel: () => void;\n\n  /**\n   * Similar to `cancel`, but would also trigger the `trailing` invocation if\n   * the debouncer would run one at the end of the cool-down period.\n   */\n  readonly flush: () => ReturnType<F> | undefined;\n\n  /**\n   * Is `true` when there is an active cool-down period currently debouncing\n   * invocations.\n   */\n  readonly isPending: boolean;\n\n  /**\n   * The last computed value of the debounced function.\n   */\n  readonly cachedValue: ReturnType<F> | undefined;\n};\n\ntype DebounceOptions = {\n  readonly waitMs?: number;\n  readonly maxWaitMs?: number;\n};\n\n/**\n * Wraps `func` with a debouncer object that \"debounces\" (delays) invocations of the function during a defined cool-down period (`waitMs`). It can be configured to invoke the function either at the start of the cool-down period, the end of it, or at both ends (`timing`).\n * It can also be configured to allow invocations during the cool-down period (`maxWaitMs`).\n * It stores the latest call's arguments so they could be used at the end of the cool-down period when invoking `func` (if configured to invoke the function at the end of the cool-down period).\n * It stores the value returned by `func` whenever its invoked. This value is returned on every call, and is accessible via the `cachedValue` property of the debouncer. Its important to note that the value might be different from the value that would be returned from running `func` with the current arguments as it is a cached value from a previous invocation.\n * **Important**: The cool-down period defines the minimum between two invocations, and not the maximum. The period will be **extended** each time a call is made until a full cool-down period has elapsed without any additional calls.\n *\n *! **DEPRECATED**: This implementation of debounce is known to have issues and might not behave as expected. It should be replaced with the `funnel` utility instead. The test file [funnel.remeda-debounce.test.ts](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts) offers a reference implementation that replicates `debounce` via `funnel`!\n *\n * @param func - The function to debounce, the returned `call` function will have\n * the exact same signature.\n * @param options - An object allowing further customization of the debouncer:\n * - `timing?: 'leading' | 'trailing' |'both'`. The default is `'trailing'`.\n *   `leading` would result in the function being invoked at the start of the\n *   cool-down period; `trailing` would result in the function being invoked at\n *   the end of the cool-down period (using the args from the last call to the\n *   debouncer). When `both` is selected the `trailing` invocation would only\n *   take place if there were more than one call to the debouncer during the\n *   cool-down period. **DEFAULT: 'trailing'**\n * - `waitMs?: number`. The length of the cool-down period in milliseconds. The\n *   debouncer would wait until this amount of time has passed without **any**\n *   additional calls to the debouncer before triggering the end-of-cool-down-\n *   period event. When this happens, the function would be invoked (if `timing`\n *   isn't `'leading'`) and the debouncer state would be reset. **DEFAULT: 0**\n * - `maxWaitMs?: number`. The length of time since a debounced call (a call\n *   that the debouncer prevented from being invoked) was made until it would be\n *   invoked. Because the debouncer can be continually triggered and thus never\n *   reach the end of the cool-down period, this allows the function to still\n *   be invoked occasionally. IMPORTANT: This param is ignored when `timing` is\n *   `'leading'`.\n * @returns A debouncer object. The main function is `call`. In addition to it\n * the debouncer comes with the following additional functions and properties:\n * - `cancel` method to cancel delayed `func` invocations\n * - `flush` method to end the cool-down period immediately.\n * - `cachedValue` the latest return value of an invocation (if one occurred).\n * - `isPending` flag to check if there is an inflight cool-down window.\n * @signature\n *   debounce(func, options);\n * @example\n *   const debouncer = debounce(identity(), { timing: 'trailing', waitMs: 1000 });\n *   const result1 = debouncer.call(1); // => undefined\n *   const result2 = debouncer.call(2); // => undefined\n *   // after 1 second\n *   const result3 = debouncer.call(3); // => 2\n *   // after 1 second\n *   debouncer.cachedValue; // => 3\n * @dataFirst\n * @category Function\n * @deprecated This implementation of debounce is known to have issues and might\n * not behave as expected. It should be replaced with the `funnel` utility\n * instead. The test file `funnel.remeda-debounce.test.ts` offers a reference\n * implementation that replicates `debounce` via `funnel`.\n * @see https://css-tricks.com/debouncing-throttling-explained-examples/\n */\nexport function debounce<F extends StrictFunction>(\n  func: F,\n  options: DebounceOptions & { readonly timing?: \"trailing\" },\n): Debouncer<F>;\nexport function debounce<F extends StrictFunction>(\n  func: F,\n  options:\n    | (DebounceOptions & { readonly timing: \"both\" })\n    | (Omit<DebounceOptions, \"maxWaitMs\"> & { readonly timing: \"leading\" }),\n): Debouncer<F, false /* call CAN'T return null */>;\n\nexport function debounce<F extends StrictFunction>(\n  func: F,\n  {\n    waitMs,\n    timing = \"trailing\",\n    maxWaitMs,\n  }: DebounceOptions & {\n    readonly timing?: \"both\" | \"leading\" | \"trailing\";\n  },\n): Debouncer<F> {\n  if (maxWaitMs !== undefined && waitMs !== undefined && maxWaitMs < waitMs) {\n    throw new Error(\n      `debounce: maxWaitMs (${maxWaitMs.toString()}) cannot be less than waitMs (${waitMs.toString()})`,\n    );\n  }\n\n  // All these are part of the debouncer runtime state:\n\n  // The timeout is the main object we use to tell if there's an active cool-\n  // down period or not.\n  let coolDownTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n  // We use an additional timeout to track how long the last debounced call is\n  // waiting.\n  let maxWaitTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n  // For 'trailing' invocations we need to keep the args around until we\n  // actually invoke the function.\n  let latestCallArgs: Parameters<F> | undefined;\n\n  // To make any value of the debounced function we need to be able to return a\n  // value. For any invocation except the first one when 'leading' is enabled we\n  // will return this cached value.\n  let result: ReturnType<F> | undefined;\n\n  const handleInvoke = (): void => {\n    if (maxWaitTimeoutId !== undefined) {\n      // We are invoking the function so the wait is over...\n      const timeoutId = maxWaitTimeoutId;\n      maxWaitTimeoutId = undefined;\n      clearTimeout(timeoutId);\n    }\n\n    /* v8 ignore if -- This protects us against changes to the logic, there is no known flow we can simulate to reach this condition. It can only happen if a previous timeout isn't cleared (or faces a race condition clearing). @preserve */\n    if (latestCallArgs === undefined) {\n      // If you see this error pop up when using this function please report\n      // it on the Remeda github page!\n      throw new Error(\n        \"REMEDA[debounce]: latestCallArgs was unexpectedly undefined.\",\n      );\n    }\n\n    const args = latestCallArgs;\n    // Make sure the args aren't accidentally used again, this is mainly\n    // relevant for the check above where we'll fail a subsequent call to\n    // 'trailingEdge'.\n    latestCallArgs = undefined;\n\n    // Invoke the function and store the results locally.\n    // @ts-expect-error [ts2345, ts2322] -- TypeScript infers the generic sub-\n    // types too eagerly, making itself blind to the fact that the types match\n    // here.\n    result = func(...args);\n  };\n\n  const handleCoolDownEnd = (): void => {\n    if (coolDownTimeoutId === undefined) {\n      // It's rare to get here, it should only happen when `flush` is called\n      // when the cool-down window isn't active.\n      return;\n    }\n\n    // Make sure there are no more timers running.\n    const timeoutId = coolDownTimeoutId;\n    coolDownTimeoutId = undefined;\n    clearTimeout(timeoutId);\n    // Then reset state so a new cool-down window can begin on the next call.\n\n    if (latestCallArgs !== undefined) {\n      // If we have a debounced call waiting to be invoked at the end of the\n      // cool-down period we need to invoke it now.\n      handleInvoke();\n    }\n  };\n\n  const handleDebouncedCall = (args: Parameters<F>): void => {\n    // We save the latest call args so that (if and) when we invoke the function\n    // in the future, we have args to invoke it with.\n    latestCallArgs = args;\n\n    if (maxWaitMs !== undefined && maxWaitTimeoutId === undefined) {\n      // We only need to start the maxWait timeout once, on the first debounced\n      // call that is now being delayed.\n      maxWaitTimeoutId = setTimeout(handleInvoke, maxWaitMs);\n    }\n  };\n\n  return {\n    call: (...args) => {\n      if (coolDownTimeoutId === undefined) {\n        // This call is starting a new cool-down window!\n\n        if (timing === \"trailing\") {\n          // Only when the timing is \"trailing\" is the first call \"debounced\".\n          handleDebouncedCall(args);\n        } else {\n          // Otherwise for \"leading\" and \"both\" the first call is actually\n          // called directly and not via a timeout.\n          // @ts-expect-error [ts2345, ts2322] -- TypeScript infers the generic\n          // sub-types too eagerly, making itself blind to the fact that the\n          // types match here.\n          result = func(...args);\n        }\n      } else {\n        // There's an inflight cool-down window.\n\n        if (timing !== \"leading\") {\n          // When the timing is 'leading' all following calls are just ignored\n          // until the cool-down period ends. But for the other timings the call\n          // is \"debounced\".\n          handleDebouncedCall(args);\n        }\n\n        // The current timeout is no longer relevant because we need to wait the\n        // full `waitMs` time from this call.\n        const timeoutId = coolDownTimeoutId;\n        coolDownTimeoutId = undefined;\n        clearTimeout(timeoutId);\n      }\n\n      coolDownTimeoutId = setTimeout(\n        handleCoolDownEnd,\n        // If waitMs is not defined but maxWaitMs *is* it means the user is only\n        // interested in the leaky-bucket nature of the debouncer which is\n        // achieved by setting waitMs === maxWaitMs. If both are not defined we\n        // default to 0 which would wait until the end of the execution frame.\n        waitMs ?? maxWaitMs ?? 0,\n      );\n\n      // Return the last computed result while we \"debounce\" further calls.\n      return result;\n    },\n\n    cancel: () => {\n      // Reset all \"in-flight\" state of the debouncer. Notice that we keep the\n      // cached value!\n\n      if (coolDownTimeoutId !== undefined) {\n        const timeoutId = coolDownTimeoutId;\n        coolDownTimeoutId = undefined;\n        clearTimeout(timeoutId);\n      }\n\n      if (maxWaitTimeoutId !== undefined) {\n        const timeoutId = maxWaitTimeoutId;\n        maxWaitTimeoutId = undefined;\n        clearTimeout(timeoutId);\n      }\n\n      latestCallArgs = undefined;\n    },\n\n    flush: () => {\n      // Flush is just a manual way to trigger the end of the cool-down window.\n      handleCoolDownEnd();\n      return result;\n    },\n\n    get isPending() {\n      return coolDownTimeoutId !== undefined;\n    },\n\n    get cachedValue() {\n      return result;\n    },\n  };\n}\n"],"mappings":"mEA+GA,SAAgB,EACd,EACA,CACE,SACA,SAAS,WACT,aAIY,CACd,GAAI,IAAc,IAAA,IAAa,IAAW,IAAA,IAAa,EAAY,EACjE,MAAU,MACR,wBAAwB,EAAU,UAAU,CAAC,gCAAgC,EAAO,UAAU,CAAC,GAChG,CAOH,IAAI,EAIA,EAIA,EAKA,EAEE,MAA2B,CAC/B,GAAI,IAAqB,IAAA,GAAW,CAElC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,EAAU,CAIzB,GAAI,IAAmB,IAAA,GAGrB,MAAU,MACR,+DACD,CAGH,IAAM,EAAO,EAIb,EAAiB,IAAA,GAMjB,EAAS,EAAK,GAAG,EAAK,EAGlB,MAAgC,CACpC,GAAI,IAAsB,IAAA,GAGxB,OAIF,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,EAAU,CAGnB,IAAmB,IAAA,IAGrB,GAAc,EAIZ,EAAuB,GAA8B,CAGzD,EAAiB,EAEb,IAAc,IAAA,IAAa,IAAqB,IAAA,KAGlD,EAAmB,WAAW,EAAc,EAAU,GAI1D,MAAO,CACL,MAAO,GAAG,IAAS,CACjB,GAAI,IAAsB,IAAA,GAGpB,IAAW,WAEb,EAAoB,EAAK,CAOzB,EAAS,EAAK,GAAG,EAAK,KAEnB,CAGD,IAAW,WAIb,EAAoB,EAAK,CAK3B,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,EAAU,CAazB,MAVA,GAAoB,WAClB,EAKA,GAAU,GAAa,EACxB,CAGM,GAGT,WAAc,CAIZ,GAAI,IAAsB,IAAA,GAAW,CACnC,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,EAAU,CAGzB,GAAI,IAAqB,IAAA,GAAW,CAClC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,EAAU,CAGzB,EAAiB,IAAA,IAGnB,WAEE,GAAmB,CACZ,GAGT,IAAI,WAAY,CACd,OAAO,IAAsB,IAAA,IAG/B,IAAI,aAAc,CAChB,OAAO,GAEV"}