@depup/remeda 2.40.0-depup.0 → 2.45.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @depup/remeda
13
13
 
14
14
  | Field | Value |
15
15
  |-------|-------|
16
- | Original | [remeda](https://www.npmjs.com/package/remeda) @ 2.40.0 |
17
- | Processed | 2026-08-16 |
16
+ | Original | [remeda](https://www.npmjs.com/package/remeda) @ 2.45.0 |
17
+ | Processed | 2026-08-30 |
18
18
  | Smoke test | passed |
19
19
  | Deps updated | 0 |
20
20
 
package/changes.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "bumped": {},
3
- "timestamp": "2026-08-16T00:27:39.570Z",
3
+ "timestamp": "2026-08-30T00:58:50.625Z",
4
4
  "totalUpdated": 0
5
5
  }
@@ -1 +1 @@
1
- {"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,SAAS,EAAE,gCAAgC,EAAO,SAAS,EAAE,EACjG,EAOF,IAAI,EAIA,EAIA,EAKA,EAEE,MAA2B,CAC/B,GAAI,IAAqB,IAAA,GAAW,CAElC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAGA,GAAI,IAAmB,IAAA,GAGrB,MAAU,MACR,8DACF,EAGF,IAAM,EAAO,EAIb,EAAiB,IAAA,GAMjB,EAAS,EAAK,GAAG,CAAI,CACvB,EAEM,MAAgC,CACpC,GAAI,IAAsB,IAAA,GAGxB,OAIF,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,EAGlB,IAAmB,IAAA,IAGrB,EAAa,CAEjB,EAEM,EAAuB,GAA8B,CAGzD,EAAiB,EAEb,IAAc,IAAA,IAAa,IAAqB,IAAA,KAGlD,EAAmB,WAAW,EAAc,CAAS,EAEzD,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CACjB,GAAI,IAAsB,IAAA,GAGpB,IAAW,WAEb,EAAoB,CAAI,EAOxB,EAAS,EAAK,GAAG,CAAI,MAElB,CAGD,IAAW,WAIb,EAAoB,CAAI,EAK1B,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAYA,MAVA,GAAoB,WAClB,EAKA,GAAU,GAAa,CACzB,EAGO,CACT,EAEA,WAAc,CAIZ,GAAI,IAAsB,IAAA,GAAW,CACnC,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAEA,GAAI,IAAqB,IAAA,GAAW,CAClC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAEA,EAAiB,IAAA,EACnB,EAEA,WAEE,EAAkB,EACX,GAGT,IAAI,WAAY,CACd,OAAO,IAAsB,IAAA,EAC/B,EAEA,IAAI,aAAc,CAChB,OAAO,CACT,CACF,CACF"}
1
+ {"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 drop-in replacement implemented via `funnel`: copy everything between its REFERENCE START and REFERENCE END markers into your project!\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](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts)\n * offers a drop-in replacement implemented via `funnel`: copy everything\n * between its REFERENCE START and REFERENCE END markers into your project.\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":"mEAgHA,SAAgB,EACd,EACA,CACE,SACA,SAAS,WACT,aAIY,CACd,GAAI,IAAc,IAAA,IAAa,IAAW,IAAA,IAAa,EAAY,EACjE,MAAU,MACR,wBAAwB,EAAU,SAAS,EAAE,gCAAgC,EAAO,SAAS,EAAE,EACjG,EAOF,IAAI,EAIA,EAIA,EAKA,EAEE,MAA2B,CAC/B,GAAI,IAAqB,IAAA,GAAW,CAElC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAGA,GAAI,IAAmB,IAAA,GAGrB,MAAU,MACR,8DACF,EAGF,IAAM,EAAO,EAIb,EAAiB,IAAA,GAMjB,EAAS,EAAK,GAAG,CAAI,CACvB,EAEM,MAAgC,CACpC,GAAI,IAAsB,IAAA,GAGxB,OAIF,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,EAGlB,IAAmB,IAAA,IAGrB,EAAa,CAEjB,EAEM,EAAuB,GAA8B,CAGzD,EAAiB,EAEb,IAAc,IAAA,IAAa,IAAqB,IAAA,KAGlD,EAAmB,WAAW,EAAc,CAAS,EAEzD,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CACjB,GAAI,IAAsB,IAAA,GAGpB,IAAW,WAEb,EAAoB,CAAI,EAOxB,EAAS,EAAK,GAAG,CAAI,MAElB,CAGD,IAAW,WAIb,EAAoB,CAAI,EAK1B,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAYA,MAVA,GAAoB,WAClB,EAKA,GAAU,GAAa,CACzB,EAGO,CACT,EAEA,WAAc,CAIZ,GAAI,IAAsB,IAAA,GAAW,CACnC,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAEA,GAAI,IAAqB,IAAA,GAAW,CAClC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAEA,EAAiB,IAAA,EACnB,EAEA,WAEE,EAAkB,EACX,GAGT,IAAI,WAAY,CACd,OAAO,IAAsB,IAAA,EAC/B,EAEA,IAAI,aAAc,CAChB,OAAO,CACT,CACF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"debounce.js","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":"AA+GA,SAAgB,EACd,EACA,CACE,SACA,SAAS,WACT,aAIY,CACd,GAAI,IAAc,IAAA,IAAa,IAAW,IAAA,IAAa,EAAY,EACjE,MAAU,MACR,wBAAwB,EAAU,SAAS,EAAE,gCAAgC,EAAO,SAAS,EAAE,EACjG,EAOF,IAAI,EAIA,EAIA,EAKA,EAEE,MAA2B,CAC/B,GAAI,IAAqB,IAAA,GAAW,CAElC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAGA,GAAI,IAAmB,IAAA,GAGrB,MAAU,MACR,8DACF,EAGF,IAAM,EAAO,EAIb,EAAiB,IAAA,GAMjB,EAAS,EAAK,GAAG,CAAI,CACvB,EAEM,MAAgC,CACpC,GAAI,IAAsB,IAAA,GAGxB,OAIF,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,EAGlB,IAAmB,IAAA,IAGrB,EAAa,CAEjB,EAEM,EAAuB,GAA8B,CAGzD,EAAiB,EAEb,IAAc,IAAA,IAAa,IAAqB,IAAA,KAGlD,EAAmB,WAAW,EAAc,CAAS,EAEzD,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CACjB,GAAI,IAAsB,IAAA,GAGpB,IAAW,WAEb,EAAoB,CAAI,EAOxB,EAAS,EAAK,GAAG,CAAI,MAElB,CAGD,IAAW,WAIb,EAAoB,CAAI,EAK1B,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAYA,MAVA,GAAoB,WAClB,EAKA,GAAU,GAAa,CACzB,EAGO,CACT,EAEA,WAAc,CAIZ,GAAI,IAAsB,IAAA,GAAW,CACnC,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAEA,GAAI,IAAqB,IAAA,GAAW,CAClC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAEA,EAAiB,IAAA,EACnB,EAEA,WAEE,EAAkB,EACX,GAGT,IAAI,WAAY,CACd,OAAO,IAAsB,IAAA,EAC/B,EAEA,IAAI,aAAc,CAChB,OAAO,CACT,CACF,CACF"}
1
+ {"version":3,"file":"debounce.js","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 drop-in replacement implemented via `funnel`: copy everything between its REFERENCE START and REFERENCE END markers into your project!\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](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts)\n * offers a drop-in replacement implemented via `funnel`: copy everything\n * between its REFERENCE START and REFERENCE END markers into your project.\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":"AAgHA,SAAgB,EACd,EACA,CACE,SACA,SAAS,WACT,aAIY,CACd,GAAI,IAAc,IAAA,IAAa,IAAW,IAAA,IAAa,EAAY,EACjE,MAAU,MACR,wBAAwB,EAAU,SAAS,EAAE,gCAAgC,EAAO,SAAS,EAAE,EACjG,EAOF,IAAI,EAIA,EAIA,EAKA,EAEE,MAA2B,CAC/B,GAAI,IAAqB,IAAA,GAAW,CAElC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAGA,GAAI,IAAmB,IAAA,GAGrB,MAAU,MACR,8DACF,EAGF,IAAM,EAAO,EAIb,EAAiB,IAAA,GAMjB,EAAS,EAAK,GAAG,CAAI,CACvB,EAEM,MAAgC,CACpC,GAAI,IAAsB,IAAA,GAGxB,OAIF,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,EAGlB,IAAmB,IAAA,IAGrB,EAAa,CAEjB,EAEM,EAAuB,GAA8B,CAGzD,EAAiB,EAEb,IAAc,IAAA,IAAa,IAAqB,IAAA,KAGlD,EAAmB,WAAW,EAAc,CAAS,EAEzD,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CACjB,GAAI,IAAsB,IAAA,GAGpB,IAAW,WAEb,EAAoB,CAAI,EAOxB,EAAS,EAAK,GAAG,CAAI,MAElB,CAGD,IAAW,WAIb,EAAoB,CAAI,EAK1B,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAYA,MAVA,GAAoB,WAClB,EAKA,GAAU,GAAa,CACzB,EAGO,CACT,EAEA,WAAc,CAIZ,GAAI,IAAsB,IAAA,GAAW,CACnC,IAAM,EAAY,EAClB,EAAoB,IAAA,GACpB,aAAa,CAAS,CACxB,CAEA,GAAI,IAAqB,IAAA,GAAW,CAClC,IAAM,EAAY,EAClB,EAAmB,IAAA,GACnB,aAAa,CAAS,CACxB,CAEA,EAAiB,IAAA,EACnB,EAEA,WAEE,EAAkB,EACX,GAGT,IAAI,WAAY,CACd,OAAO,IAAsB,IAAA,EAC/B,EAEA,IAAI,aAAc,CAChB,OAAO,CACT,CACF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"filter.cjs","names":["purry","SKIP_ITEM"],"sources":["../src/filter.ts"],"sourcesContent":["import type { Writable } from \"type-fest\";\nimport type { FilteredArray } from \"./internal/types/FilteredArray\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\nimport type { LazyEvaluator } from \"./internal/types/LazyEvaluator\";\nimport { SKIP_ITEM } from \"./internal/utilityEvaluators\";\nimport { purry } from \"./purry\";\n\n// When the predicate used for filter isn't refining (like a type-predicate) we\n// can narrow the result slightly if it's also trivial (it returns the same\n// result for all items). This is uncommon, but can be useful to \"short-circuit\"\n// the filter.\ntype NonRefinedFilteredArray<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n> = boolean extends IsItemIncluded\n ? // We don't know which items of the array the predicate would allow in the\n // output so we can only safely say that the result is an array with items\n // from the input array.\n // TODO: Theoretically we could build an output shape that would take into account the **order** of elements in the input array by reconstructing it with every single element in it either included or not, but this type can grow to a union of as much as 2^n options which might not be usable in practice.\n T[number][]\n : IsItemIncluded extends true\n ? // If the predicate is always true we return a shallow copy of the array.\n // If it was originally readonly we need to strip that away.\n Writable<T>\n : // If the predicate is always false we will always return an empty\n // array.\n [];\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param data - The array to filter.\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(data, predicate)\n * @example\n * filter([1, 2, 3], x => x % 2 === 1) // => [1, 3]\n * @dataFirst\n * @lazy\n * @category Array\n */\nexport function filter<\n T extends IterableContainer,\n Condition extends T[number],\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): NonRefinedFilteredArray<T, IsItemIncluded>;\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(predicate)(data)\n * @example\n * pipe([1, 2, 3], filter(x => x % 2 === 1)) // => [1, 3]\n * @dataLast\n * @lazy\n * @category Array\n */\nexport function filter<\n T extends IterableContainer,\n Condition extends T[number],\n>(\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): (data: T) => FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): (data: T) => NonRefinedFilteredArray<T, IsItemIncluded>;\n\nexport function filter(...args: readonly unknown[]): unknown {\n return purry(filterImplementation, args, lazyImplementation);\n}\n\nconst filterImplementation = <T>(\n data: readonly T[],\n predicate: (value: T, index: number, array: readonly T[]) => boolean,\n): T[] => data.filter(predicate);\n\nconst lazyImplementation =\n <T>(\n predicate: (value: T, index: number, data: readonly T[]) => boolean,\n ): LazyEvaluator<T> =>\n (value, index, data) =>\n predicate(value, index, data)\n ? { done: false, hasNext: true, next: value }\n : SKIP_ITEM;\n"],"mappings":"gJAmGA,SAAgB,EAAO,GAAG,EAAmC,CAC3D,OAAOA,EAAAA,MAAM,EAAsB,EAAM,CAAkB,CAC7D,CAEA,MAAM,GACJ,EACA,IACQ,EAAK,OAAO,CAAS,EAEzB,EAEF,IAED,EAAO,EAAO,IACb,EAAU,EAAO,EAAO,CAAI,EACxB,CAAE,KAAM,GAAO,QAAS,GAAM,KAAM,CAAM,EAC1CC,EAAAA"}
1
+ {"version":3,"file":"filter.cjs","names":["purry","SKIP_ITEM"],"sources":["../src/filter.ts"],"sourcesContent":["import type { Writable } from \"type-fest\";\nimport type { FilteredArray } from \"./internal/types/FilteredArray\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\nimport type { LazyEvaluator } from \"./internal/types/LazyEvaluator\";\nimport { SKIP_ITEM } from \"./internal/utilityEvaluators\";\nimport { purry } from \"./purry\";\n\n// When the predicate used for filter isn't refining (like a type-predicate) we\n// can narrow the result slightly if it's also trivial (it returns the same\n// result for all items). This is uncommon, but can be useful to \"short-circuit\"\n// the filter.\ntype NonRefinedFilteredArray<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n> = boolean extends IsItemIncluded\n ? // We don't know which items of the array the predicate would allow in the\n // output so we can only safely say that the result is an array with items\n // from the input array.\n // TODO: Theoretically we could build an output shape that would take into account the **order** of elements in the input array by reconstructing it with every single element in it either included or not, but this type can grow to a union of as much as 2^n options which might not be usable in practice.\n T[number][]\n : IsItemIncluded extends true\n ? // If the predicate is always true we return a shallow copy of the array.\n // If it was originally readonly we need to strip that away.\n Writable<T>\n : // If the predicate is always false we will always return an empty\n // array.\n [];\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param data - The array to filter.\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(data, predicate)\n * @example\n * filter([1, 2, 3], x => x % 2 === 1) // => [1, 3]\n * @dataFirst\n * @lazy\n * @category Array\n */\nexport function filter<T extends IterableContainer, Condition>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): NonRefinedFilteredArray<T, IsItemIncluded>;\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(predicate)(data)\n * @example\n * pipe([1, 2, 3], filter(x => x % 2 === 1)) // => [1, 3]\n * @dataLast\n * @lazy\n * @category Array\n */\nexport function filter<T extends IterableContainer, Condition>(\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): (data: T) => FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): (data: T) => NonRefinedFilteredArray<T, IsItemIncluded>;\n\nexport function filter(...args: readonly unknown[]): unknown {\n return purry(filterImplementation, args, lazyImplementation);\n}\n\nconst filterImplementation = <T>(\n data: readonly T[],\n predicate: (value: T, index: number, array: readonly T[]) => boolean,\n): T[] => data.filter(predicate);\n\nconst lazyImplementation =\n <T>(\n predicate: (value: T, index: number, data: readonly T[]) => boolean,\n ): LazyEvaluator<T> =>\n (value, index, data) =>\n predicate(value, index, data)\n ? { done: false, hasNext: true, next: value }\n : SKIP_ITEM;\n"],"mappings":"gJA6FA,SAAgB,EAAO,GAAG,EAAmC,CAC3D,OAAOA,EAAAA,MAAM,EAAsB,EAAM,CAAkB,CAC7D,CAEA,MAAM,GACJ,EACA,IACQ,EAAK,OAAO,CAAS,EAEzB,EAEF,IAED,EAAO,EAAO,IACb,EAAU,EAAO,EAAO,CAAI,EACxB,CAAE,KAAM,GAAO,QAAS,GAAM,KAAM,CAAM,EAC1CC,EAAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"filter.js","names":[],"sources":["../src/filter.ts"],"sourcesContent":["import type { Writable } from \"type-fest\";\nimport type { FilteredArray } from \"./internal/types/FilteredArray\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\nimport type { LazyEvaluator } from \"./internal/types/LazyEvaluator\";\nimport { SKIP_ITEM } from \"./internal/utilityEvaluators\";\nimport { purry } from \"./purry\";\n\n// When the predicate used for filter isn't refining (like a type-predicate) we\n// can narrow the result slightly if it's also trivial (it returns the same\n// result for all items). This is uncommon, but can be useful to \"short-circuit\"\n// the filter.\ntype NonRefinedFilteredArray<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n> = boolean extends IsItemIncluded\n ? // We don't know which items of the array the predicate would allow in the\n // output so we can only safely say that the result is an array with items\n // from the input array.\n // TODO: Theoretically we could build an output shape that would take into account the **order** of elements in the input array by reconstructing it with every single element in it either included or not, but this type can grow to a union of as much as 2^n options which might not be usable in practice.\n T[number][]\n : IsItemIncluded extends true\n ? // If the predicate is always true we return a shallow copy of the array.\n // If it was originally readonly we need to strip that away.\n Writable<T>\n : // If the predicate is always false we will always return an empty\n // array.\n [];\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param data - The array to filter.\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(data, predicate)\n * @example\n * filter([1, 2, 3], x => x % 2 === 1) // => [1, 3]\n * @dataFirst\n * @lazy\n * @category Array\n */\nexport function filter<\n T extends IterableContainer,\n Condition extends T[number],\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): NonRefinedFilteredArray<T, IsItemIncluded>;\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(predicate)(data)\n * @example\n * pipe([1, 2, 3], filter(x => x % 2 === 1)) // => [1, 3]\n * @dataLast\n * @lazy\n * @category Array\n */\nexport function filter<\n T extends IterableContainer,\n Condition extends T[number],\n>(\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): (data: T) => FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): (data: T) => NonRefinedFilteredArray<T, IsItemIncluded>;\n\nexport function filter(...args: readonly unknown[]): unknown {\n return purry(filterImplementation, args, lazyImplementation);\n}\n\nconst filterImplementation = <T>(\n data: readonly T[],\n predicate: (value: T, index: number, array: readonly T[]) => boolean,\n): T[] => data.filter(predicate);\n\nconst lazyImplementation =\n <T>(\n predicate: (value: T, index: number, data: readonly T[]) => boolean,\n ): LazyEvaluator<T> =>\n (value, index, data) =>\n predicate(value, index, data)\n ? { done: false, hasNext: true, next: value }\n : SKIP_ITEM;\n"],"mappings":"kFAmGA,SAAgB,EAAO,GAAG,EAAmC,CAC3D,OAAO,EAAM,EAAsB,EAAM,CAAkB,CAC7D,CAEA,MAAM,GACJ,EACA,IACQ,EAAK,OAAO,CAAS,EAEzB,EAEF,IAED,EAAO,EAAO,IACb,EAAU,EAAO,EAAO,CAAI,EACxB,CAAE,KAAM,GAAO,QAAS,GAAM,KAAM,CAAM,EAC1C"}
1
+ {"version":3,"file":"filter.js","names":[],"sources":["../src/filter.ts"],"sourcesContent":["import type { Writable } from \"type-fest\";\nimport type { FilteredArray } from \"./internal/types/FilteredArray\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\nimport type { LazyEvaluator } from \"./internal/types/LazyEvaluator\";\nimport { SKIP_ITEM } from \"./internal/utilityEvaluators\";\nimport { purry } from \"./purry\";\n\n// When the predicate used for filter isn't refining (like a type-predicate) we\n// can narrow the result slightly if it's also trivial (it returns the same\n// result for all items). This is uncommon, but can be useful to \"short-circuit\"\n// the filter.\ntype NonRefinedFilteredArray<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n> = boolean extends IsItemIncluded\n ? // We don't know which items of the array the predicate would allow in the\n // output so we can only safely say that the result is an array with items\n // from the input array.\n // TODO: Theoretically we could build an output shape that would take into account the **order** of elements in the input array by reconstructing it with every single element in it either included or not, but this type can grow to a union of as much as 2^n options which might not be usable in practice.\n T[number][]\n : IsItemIncluded extends true\n ? // If the predicate is always true we return a shallow copy of the array.\n // If it was originally readonly we need to strip that away.\n Writable<T>\n : // If the predicate is always false we will always return an empty\n // array.\n [];\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param data - The array to filter.\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(data, predicate)\n * @example\n * filter([1, 2, 3], x => x % 2 === 1) // => [1, 3]\n * @dataFirst\n * @lazy\n * @category Array\n */\nexport function filter<T extends IterableContainer, Condition>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n data: T,\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): NonRefinedFilteredArray<T, IsItemIncluded>;\n\n/**\n * Creates a shallow copy of a portion of a given array, filtered down to just\n * the elements from the given array that pass the test implemented by the\n * provided function. Equivalent to `Array.prototype.filter`.\n *\n * Related operations:\n * - `splice` - to shape the array by *position* rather than by *value*.\n *\n * @param predicate - A function to execute for each element in the array. It\n * should return `true` to keep the element in the resulting array, and `false`\n * otherwise. A type-predicate can also be used to narrow the result.\n * @returns A shallow copy of the given array containing just the elements that\n * pass the test. If no elements pass the test, an empty array is returned.\n * @signature\n * filter(predicate)(data)\n * @example\n * pipe([1, 2, 3], filter(x => x % 2 === 1)) // => [1, 3]\n * @dataLast\n * @lazy\n * @category Array\n */\nexport function filter<T extends IterableContainer, Condition>(\n predicate: (value: T[number], index: number, data: T) => value is Condition,\n): (data: T) => FilteredArray<T, Condition>;\nexport function filter<\n T extends IterableContainer,\n IsItemIncluded extends boolean,\n>(\n predicate: (value: T[number], index: number, data: T) => IsItemIncluded,\n): (data: T) => NonRefinedFilteredArray<T, IsItemIncluded>;\n\nexport function filter(...args: readonly unknown[]): unknown {\n return purry(filterImplementation, args, lazyImplementation);\n}\n\nconst filterImplementation = <T>(\n data: readonly T[],\n predicate: (value: T, index: number, array: readonly T[]) => boolean,\n): T[] => data.filter(predicate);\n\nconst lazyImplementation =\n <T>(\n predicate: (value: T, index: number, data: readonly T[]) => boolean,\n ): LazyEvaluator<T> =>\n (value, index, data) =>\n predicate(value, index, data)\n ? { done: false, hasNext: true, next: value }\n : SKIP_ITEM;\n"],"mappings":"kFA6FA,SAAgB,EAAO,GAAG,EAAmC,CAC3D,OAAO,EAAM,EAAsB,EAAM,CAAkB,CAC7D,CAEA,MAAM,GACJ,EACA,IACQ,EAAK,OAAO,CAAS,EAEzB,EAEF,IAED,EAAO,EAAO,IACb,EAAU,EAAO,EAAO,CAAI,EACxB,CAAE,KAAM,GAAO,QAAS,GAAM,KAAM,CAAM,EAC1C"}
@@ -1 +1 @@
1
- {"version":3,"file":"funnel.cjs","names":[],"sources":["../src/funnel.ts"],"sourcesContent":["import type { RequireAtLeastOne } from \"type-fest\";\n\n// We use the value provided by the reducer to also determine if a call\n// was done during a timeout period. This means that even when no reducer\n// is provided, we still need a dummy reducer that would return something\n// other than `undefined`. It is safe to cast this to R (which might be\n// anything) because the callback would never use it as it would be typed\n// as a zero-args function.\nconst VOID_REDUCER_SYMBOL = Symbol(\"funnel/voidReducer\");\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- Intentional, so that it could be used as a default value above.\nconst voidReducer = <R>(): R => VOID_REDUCER_SYMBOL as R;\n\ntype FunnelOptions<Args extends RestArguments, R> = {\n readonly reducer?: (accumulator: R | undefined, ...params: Args) => R;\n} & FunnelTimingOptions;\n\n// Not all combinations of timing options are valid, there are dependencies\n// between them to ensure users can't configure the funnel in a way which would\n// cause it to never trigger.\ntype FunnelTimingOptions =\n | ({ readonly triggerAt?: \"end\" } & (\n | ({ readonly minGapMs: number } & RequireAtLeastOne<{\n readonly minQuietPeriodMs: number;\n readonly maxBurstDurationMs: number;\n }>)\n | {\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: never;\n }\n ))\n | {\n readonly triggerAt: \"start\" | \"both\";\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: number;\n };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeScript has some quirks with generic function types, and works best with `any` and not `unknown`. This follows the typing of built-in utilities like `ReturnType` and `Parameters`.\ntype RestArguments = any[];\n\ntype Funnel<Args extends RestArguments = []> = {\n /**\n * Call the function. This might result in the `execute` function being called\n * now or later, depending on it's configuration and it's current state.\n *\n * @param args - The args are defined by the `reducer` function.\n */\n\n readonly call: (...args: Args) => void;\n\n /**\n * Resets the funnel to it's initial state. Any calls made since the last\n * invocation will be discarded.\n */\n readonly cancel: () => void;\n\n /**\n * Triggers an invocation regardless of the current state of the funnel.\n * Like any other invocation, The funnel will also be reset to it's initial\n * state afterwards.\n */\n readonly flush: () => void;\n\n /**\n * The funnel is in it's initial state (there are no active timeouts).\n */\n readonly isIdle: boolean;\n};\n\n/**\n * Creates a funnel that controls the timing and execution of `callback`. Its\n * main purpose is to manage multiple consecutive (usually fast-paced) calls,\n * reshaping them according to a defined batching strategy and timing policy.\n * This is useful when handling uncontrolled call rates, such as DOM events or\n * network traffic. It can implement strategies like debouncing, throttling,\n * batching, and more.\n *\n * An optional `reducer` function can be provided to allow passing data to the\n * callback via calls to `call` (otherwise the signature of `call` takes no\n * arguments).\n *\n * Typing is inferred from `callback`s param, and from the rest params that\n * the optional `reducer` function accepts. Use **explicit** types for these\n * to ensure that everything _else_ is well-typed.\n *\n * Notice that this function constructs a funnel **object**, and does **not**\n * execute anything when called. The returned object should be used to execute\n * the funnel via the its `call` method.\n *\n * - Debouncing: use `minQuietPeriodMs` and any `triggerAt`.\n * - Throttling: use `minGapMs` and `triggerAt: \"start\"` or `\"both\"`.\n * - Batching: See the reference implementation in [`funnel.reference-batch.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.reference-batch.test.ts).\n *\n * @param callback - The main function that would be invoked periodically based\n * on `options`. The function would take the latest result of the `reducer`; if\n * no calls where made since the last time it was invoked it will not be\n * invoked. (If a return value is needed, it should be passed via a reference or\n * via closure to the outer scope of the funnel).\n * @param options - An object that defines when `execute` should be invoked,\n * relative to the calls of `call`. An empty/missing options object is\n * equivalent to setting `minQuietPeriodMs` to `0`.\n * @param options.reducer - Combines the arguments passed to `call` with the\n * value computed on the previous call (or `undefined` on the first time). The\n * goal of the function is to extract and summarize the data needed for\n * `callback`. It should be fast and simple as it is called often and should\n * defer heavy operations to the `execute` function. If the final value\n * is `undefined`, `callback` will not be called.\n * @param options.triggerAt - At what \"edges\" of the funnel's burst window\n * would `execute` invoke:\n * - `start` - the function will be invoked immediately (within the **same**\n * execution frame!), and any subsequent calls would be ignored until the funnel\n * is idle again. During this period `reducer` will also not be called.\n * - `end` - the function will **not** be invoked initially but the timer will\n * be started. Any calls during this time would be passed to the reducer, and\n * when the timers are done, the reduced result would trigger an invocation.\n * - `both` - the function will be invoked immediately, and then the funnel\n * would behave as if it was in the 'end' state. Default: 'end'.\n * @param options.minQuietPeriodMs - The burst timer prevents subsequent calls\n * in short succession to cause excessive invocations (aka \"debounce\"). This\n * duration represents the **minimum** amount of time that needs to pass\n * between calls (the \"quiet\" part) in order for the subsequent call to **not**\n * be considered part of the burst. In other words, as long as calls are faster\n * than this, they are considered part of the burst and the burst is extended.\n * @param options.maxBurstDurationMs - Bursts are extended every time a call is\n * made within the burst period. This means that the burst period could be\n * extended indefinitely. To prevent such cases, a maximum burst duration could\n * be defined. When `minQuietPeriodMs` is not defined and this option is, they\n * will both share the same value.\n * @param options.minGapMs - A minimum duration between calls of `execute`.\n * This is maintained regardless of the shape of the burst and is ensured even\n * if the `maxBurstDurationMs` is reached before it. (aka \"throttle\").\n * @returns A funnel with a `call` function that is used to trigger invocations.\n * In addition to it the funnel also comes with the following functions and\n * properties:\n * - `cancel` - Resets the funnel to it's initial state, discarding the current\n * `reducer` result without calling `execute` on it.\n * - `flush` - Triggers an invocation even if there are active timeouts, and\n * then resets the funnel to it's initial state.\n * - `isIdle` - Checks if there are any active timeouts.\n * @signature\n * funnel(callback, options);\n * @example\n * const debouncer = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minQuietPeriodMs: 100 },\n * );\n * debouncer.call();\n * debouncer.call();\n *\n * const throttle = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minGapMs: 100, triggerAt: \"start\" },\n * );\n * throttle.call();\n * throttle.call();\n * @category Function\n */\nexport function funnel<Args extends RestArguments = [], R = never>(\n callback: (data: R) => void,\n {\n triggerAt = \"end\",\n minQuietPeriodMs,\n maxBurstDurationMs,\n minGapMs,\n reducer = voidReducer,\n }: FunnelOptions<Args, R>,\n): Funnel<Args> {\n // We manage execution via 2 timeouts, one to track bursts of calls, and one\n // to track the interval between invocations. Together we refer to the period\n // where any of these are active as a \"cool-down period\".\n let burstTimeoutId: ReturnType<typeof setTimeout> | undefined;\n let intervalTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Until invoked, all calls are reduced into a single value that would be sent\n // to the executor on invocation.\n let preparedData: R | undefined;\n\n // In order to be able to limit the total size of the burst (when\n // `maxBurstDurationMs` is used) we need to track when the burst started.\n let burstStartTimestamp: number | undefined;\n\n const invoke = (): void => {\n const param = preparedData;\n if (param === undefined) {\n // There were no calls during both cool-down periods.\n return;\n }\n\n // Make sure the args aren't accidentally used again\n preparedData = undefined;\n\n if (param === VOID_REDUCER_SYMBOL) {\n // @ts-expect-error [ts2554] -- R is typed as `never` because we hide the\n // symbol that `voidReducer` returns; there's no way to make TypeScript\n // aware of this.\n callback();\n } else {\n callback(param);\n }\n\n if (minGapMs !== undefined) {\n intervalTimeoutId = setTimeout(handleIntervalEnd, minGapMs);\n }\n };\n\n const handleIntervalEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n if (burstTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n const handleBurstEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n if (intervalTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n return {\n call: (...args) => {\n // We act based on the initial state of the timeouts before the call is\n // handled and causes the timeouts to change.\n const wasIdle =\n burstTimeoutId === undefined && intervalTimeoutId === undefined;\n\n if (triggerAt !== \"start\" || wasIdle) {\n preparedData = reducer(preparedData, ...args);\n }\n\n if (burstTimeoutId === undefined && !wasIdle) {\n // We are not in an active burst period but in an interval period. We\n // don't start a new burst window until the next invoke.\n return;\n }\n\n if (\n minQuietPeriodMs !== undefined ||\n maxBurstDurationMs !== undefined ||\n minGapMs === undefined\n ) {\n // The timeout tracking the burst period needs to be reset every time\n // another call is made so that it waits the full cool-down duration\n // before it is released.\n clearTimeout(burstTimeoutId);\n\n const now = Date.now();\n\n burstStartTimestamp ??= now;\n\n const burstRemainingMs =\n maxBurstDurationMs === undefined\n ? (minQuietPeriodMs ?? 0)\n : Math.min(\n minQuietPeriodMs ?? maxBurstDurationMs,\n // We need to account for the time already spent so that we\n // don't wait longer than the maxDelay.\n Math.max(0, maxBurstDurationMs - (now - burstStartTimestamp)),\n );\n\n burstTimeoutId = setTimeout(handleBurstEnd, burstRemainingMs);\n }\n\n if (triggerAt !== \"end\" && wasIdle) {\n invoke();\n }\n },\n\n cancel: () => {\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n preparedData = undefined;\n },\n\n flush: () => {\n handleBurstEnd();\n handleIntervalEnd();\n },\n\n get isIdle() {\n return burstTimeoutId === undefined && intervalTimeoutId === undefined;\n },\n };\n}\n"],"mappings":"mEAQA,MAAM,EAAsB,OAAO,oBAAoB,EAEjD,MAA0B,EAwJhC,SAAgB,EACd,EACA,CACE,YAAY,MACZ,mBACA,qBACA,WACA,UAAU,GAEE,CAId,IAAI,EACA,EAIA,EAIA,EAEE,MAAqB,CACzB,IAAM,EAAQ,EACV,IAAU,IAAA,KAMd,EAAe,IAAA,GAEX,IAAU,EAIZ,EAAS,EAET,EAAS,CAAK,EAGZ,IAAa,IAAA,KACf,EAAoB,WAAW,EAAmB,CAAQ,GAE9D,EAEM,MAAgC,CAGpC,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEhB,IAAmB,IAAA,IAOvB,EAAO,CACT,EAEM,MAA6B,CAGjC,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAElB,IAAsB,IAAA,IAO1B,EAAO,CACT,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CAGjB,IAAM,EACJ,IAAmB,IAAA,IAAa,IAAsB,IAAA,GAExD,IAAI,IAAc,SAAW,KAC3B,EAAe,EAAQ,EAAc,GAAG,CAAI,GAG1C,MAAmB,IAAA,IAAa,CAAC,GAMrC,IACE,IAAqB,IAAA,IACrB,IAAuB,IAAA,IACvB,IAAa,IAAA,GACb,CAIA,aAAa,CAAc,EAE3B,IAAM,EAAM,KAAK,IAAI,EAErB,IAAwB,EAExB,IAAM,EACJ,IAAuB,IAAA,GAClB,GAAoB,EACrB,KAAK,IACH,GAAoB,EAGpB,KAAK,IAAI,EAAG,GAAsB,EAAM,EAAoB,CAC9D,EAEN,EAAiB,WAAW,EAAgB,CAAgB,CAC9D,CAEI,IAAc,OAAS,GACzB,EAAO,CAHT,CAKF,EAEA,WAAc,CACZ,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAEtB,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEpB,EAAe,IAAA,EACjB,EAEA,UAAa,CACX,EAAe,EACf,EAAkB,CACpB,EAEA,IAAI,QAAS,CACX,OAAO,IAAmB,IAAA,IAAa,IAAsB,IAAA,EAC/D,CACF,CACF"}
1
+ {"version":3,"file":"funnel.cjs","names":[],"sources":["../src/funnel.ts"],"sourcesContent":["import type { RequireAtLeastOne } from \"type-fest\";\n\n// We use the value provided by the reducer to also determine if a call\n// was done during a timeout period. This means that even when no reducer\n// is provided, we still need a dummy reducer that would return something\n// other than `undefined`. It is safe to cast this to R (which might be\n// anything) because the callback would never use it as it would be typed\n// as a zero-args function.\nconst VOID_REDUCER_SYMBOL = Symbol(\"funnel/voidReducer\");\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- Intentional, so that it could be used as a default value above.\nconst voidReducer = <R>(): R => VOID_REDUCER_SYMBOL as R;\n\ntype FunnelOptions<Args extends RestArguments, R> = {\n readonly reducer?: (accumulator: R | undefined, ...params: Args) => R;\n} & FunnelTimingOptions;\n\n// Not all combinations of timing options are valid, there are dependencies\n// between them to ensure users can't configure the funnel in a way which would\n// cause it to never trigger.\ntype FunnelTimingOptions =\n | ({ readonly triggerAt?: \"end\" } & (\n | ({ readonly minGapMs: number } & RequireAtLeastOne<{\n readonly minQuietPeriodMs: number;\n readonly maxBurstDurationMs: number;\n }>)\n | {\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: never;\n }\n ))\n | {\n readonly triggerAt: \"start\" | \"both\";\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: number;\n };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeScript has some quirks with generic function types, and works best with `any` and not `unknown`. This follows the typing of built-in utilities like `ReturnType` and `Parameters`.\ntype RestArguments = any[];\n\ntype Funnel<Args extends RestArguments = []> = {\n /**\n * Call the function. This might result in the `execute` function being called\n * now or later, depending on it's configuration and it's current state.\n *\n * @param args - The args are defined by the `reducer` function.\n */\n\n readonly call: (...args: Args) => void;\n\n /**\n * Resets the funnel to it's initial state. Any calls made since the last\n * invocation will be discarded.\n */\n readonly cancel: () => void;\n\n /**\n * Triggers an invocation regardless of the current state of the funnel.\n * Like any other invocation, The funnel will also be reset to it's initial\n * state afterwards.\n */\n readonly flush: () => void;\n\n /**\n * The funnel is in it's initial state (there are no active timeouts).\n */\n readonly isIdle: boolean;\n};\n\n/**\n * Creates a funnel that controls the timing and execution of `callback`. Its\n * main purpose is to manage multiple consecutive (usually fast-paced) calls,\n * reshaping them according to a defined batching strategy and timing policy.\n * This is useful when handling uncontrolled call rates, such as DOM events or\n * network traffic. It can implement strategies like debouncing, throttling,\n * batching, and more.\n *\n * An optional `reducer` function can be provided to allow passing data to the\n * callback via calls to `call` (otherwise the signature of `call` takes no\n * arguments).\n *\n * Typing is inferred from `callback`s param, and from the rest params that\n * the optional `reducer` function accepts. Use **explicit** types for these\n * to ensure that everything _else_ is well-typed.\n *\n * Notice that this function constructs a funnel **object**, and does **not**\n * execute anything when called. The returned object should be used to execute\n * the funnel via the its `call` method.\n *\n * The type of the funnel object is not exported; when you need to reference\n * it explicitly (e.g., a class property holding a funnel) use\n * `ReturnType<typeof funnel<[]>>`, replacing `[]` with the `reducer`'s rest\n * params when one is used (e.g., `ReturnType<typeof funnel<[string]>>`).\n *\n * - Debouncing: use `minQuietPeriodMs` and any `triggerAt`. Copy-paste\n * reference implementations are available for Remeda's deprecated `debounce`\n * function in [`funnel.remeda-debounce.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts),\n * and for the Lodash `debounce` function in the [Lodash migration docs](https://remedajs.com/migrate/lodash#debounce).\n * - Throttling: use `minGapMs` and `triggerAt: \"start\"` or `\"both\"`.\n * Copy-paste reference implementations for the Lodash `throttle` function\n * (which maps onto the burst options `minQuietPeriodMs` and\n * `maxBurstDurationMs` instead of `minGapMs`) are available in the\n * [Lodash migration docs](https://remedajs.com/migrate/lodash#throttle).\n * - Batching: a copy-paste reference implementation is available in [`funnel.reference-batch.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.reference-batch.test.ts).\n *\n * @param callback - The main function that would be invoked periodically based\n * on `options`. The function would take the latest result of the `reducer`; if\n * no calls where made since the last time it was invoked it will not be\n * invoked. (If a return value is needed, it should be passed via a reference or\n * via closure to the outer scope of the funnel).\n * @param options - An object that defines when `execute` should be invoked,\n * relative to the calls of `call`. An empty/missing options object is\n * equivalent to setting `minQuietPeriodMs` to `0`.\n * @param options.reducer - Combines the arguments passed to `call` with the\n * value computed on the previous call (or `undefined` on the first time). The\n * goal of the function is to extract and summarize the data needed for\n * `callback`. It should be fast and simple as it is called often and should\n * defer heavy operations to the `execute` function. If the final value\n * is `undefined`, `callback` will not be called.\n * @param options.triggerAt - At what \"edges\" of the funnel's burst window\n * would `execute` invoke:\n * - `start` - the function will be invoked immediately (within the **same**\n * execution frame!), and any subsequent calls would be ignored until the funnel\n * is idle again. During this period `reducer` will also not be called.\n * - `end` - the function will **not** be invoked initially but the timer will\n * be started. Any calls during this time would be passed to the reducer, and\n * when the timers are done, the reduced result would trigger an invocation.\n * - `both` - the function will be invoked immediately, and then the funnel\n * would behave as if it was in the 'end' state. Default: 'end'.\n * @param options.minQuietPeriodMs - The burst timer prevents subsequent calls\n * in short succession to cause excessive invocations (aka \"debounce\"). This\n * duration represents the **minimum** amount of time that needs to pass\n * between calls (the \"quiet\" part) in order for the subsequent call to **not**\n * be considered part of the burst. In other words, as long as calls are faster\n * than this, they are considered part of the burst and the burst is extended.\n * @param options.maxBurstDurationMs - Bursts are extended every time a call is\n * made within the burst period. This means that the burst period could be\n * extended indefinitely. To prevent such cases, a maximum burst duration could\n * be defined. When `minQuietPeriodMs` is not defined and this option is, they\n * will both share the same value.\n * @param options.minGapMs - A minimum duration between calls of `execute`.\n * This is maintained regardless of the shape of the burst and is ensured even\n * if the `maxBurstDurationMs` is reached before it. (aka \"throttle\").\n * @returns A funnel with a `call` function that is used to trigger invocations.\n * In addition to it the funnel also comes with the following functions and\n * properties:\n * - `cancel` - Resets the funnel to it's initial state, discarding the current\n * `reducer` result without calling `execute` on it.\n * - `flush` - Triggers an invocation even if there are active timeouts, and\n * then resets the funnel to it's initial state.\n * - `isIdle` - Checks if there are any active timeouts.\n * @signature\n * funnel(callback, options);\n * @example\n * const debouncer = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minQuietPeriodMs: 100 },\n * );\n * debouncer.call();\n * debouncer.call();\n *\n * const throttle = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minGapMs: 100, triggerAt: \"start\" },\n * );\n * throttle.call();\n * throttle.call();\n * @category Function\n */\nexport function funnel<Args extends RestArguments = [], R = never>(\n callback: (data: R) => void,\n {\n triggerAt = \"end\",\n minQuietPeriodMs,\n maxBurstDurationMs,\n minGapMs,\n reducer = voidReducer,\n }: FunnelOptions<Args, R>,\n): Funnel<Args> {\n // We manage execution via 2 timeouts, one to track bursts of calls, and one\n // to track the interval between invocations. Together we refer to the period\n // where any of these are active as a \"cool-down period\".\n let burstTimeoutId: ReturnType<typeof setTimeout> | undefined;\n let intervalTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Until invoked, all calls are reduced into a single value that would be sent\n // to the executor on invocation.\n let preparedData: R | undefined;\n\n // In order to be able to limit the total size of the burst (when\n // `maxBurstDurationMs` is used) we need to track when the burst started.\n let burstStartTimestamp: number | undefined;\n\n const invoke = (): void => {\n const param = preparedData;\n if (param === undefined) {\n // There were no calls during both cool-down periods.\n return;\n }\n\n // Make sure the args aren't accidentally used again\n preparedData = undefined;\n\n if (param === VOID_REDUCER_SYMBOL) {\n // @ts-expect-error [ts2554] -- R is typed as `never` because we hide the\n // symbol that `voidReducer` returns; there's no way to make TypeScript\n // aware of this.\n callback();\n } else {\n callback(param);\n }\n\n if (minGapMs !== undefined) {\n intervalTimeoutId = setTimeout(handleIntervalEnd, minGapMs);\n }\n };\n\n const handleIntervalEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n if (burstTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n const handleBurstEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n if (intervalTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n return {\n call: (...args) => {\n // We act based on the initial state of the timeouts before the call is\n // handled and causes the timeouts to change.\n const wasIdle =\n burstTimeoutId === undefined && intervalTimeoutId === undefined;\n\n if (triggerAt !== \"start\" || wasIdle) {\n preparedData = reducer(preparedData, ...args);\n }\n\n if (burstTimeoutId === undefined && !wasIdle) {\n // We are not in an active burst period but in an interval period. We\n // don't start a new burst window until the next invoke.\n return;\n }\n\n if (\n minQuietPeriodMs !== undefined ||\n maxBurstDurationMs !== undefined ||\n minGapMs === undefined\n ) {\n // The timeout tracking the burst period needs to be reset every time\n // another call is made so that it waits the full cool-down duration\n // before it is released.\n clearTimeout(burstTimeoutId);\n\n const now = Date.now();\n\n burstStartTimestamp ??= now;\n\n const burstRemainingMs =\n maxBurstDurationMs === undefined\n ? (minQuietPeriodMs ?? 0)\n : Math.min(\n minQuietPeriodMs ?? maxBurstDurationMs,\n // We need to account for the time already spent so that we\n // don't wait longer than the maxDelay.\n Math.max(0, maxBurstDurationMs - (now - burstStartTimestamp)),\n );\n\n burstTimeoutId = setTimeout(handleBurstEnd, burstRemainingMs);\n }\n\n if (triggerAt !== \"end\" && wasIdle) {\n invoke();\n }\n },\n\n cancel: () => {\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n preparedData = undefined;\n },\n\n flush: () => {\n handleBurstEnd();\n handleIntervalEnd();\n },\n\n get isIdle() {\n return burstTimeoutId === undefined && intervalTimeoutId === undefined;\n },\n };\n}\n"],"mappings":"mEAQA,MAAM,EAAsB,OAAO,oBAAoB,EAEjD,MAA0B,EAoKhC,SAAgB,EACd,EACA,CACE,YAAY,MACZ,mBACA,qBACA,WACA,UAAU,GAEE,CAId,IAAI,EACA,EAIA,EAIA,EAEE,MAAqB,CACzB,IAAM,EAAQ,EACV,IAAU,IAAA,KAMd,EAAe,IAAA,GAEX,IAAU,EAIZ,EAAS,EAET,EAAS,CAAK,EAGZ,IAAa,IAAA,KACf,EAAoB,WAAW,EAAmB,CAAQ,GAE9D,EAEM,MAAgC,CAGpC,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEhB,IAAmB,IAAA,IAOvB,EAAO,CACT,EAEM,MAA6B,CAGjC,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAElB,IAAsB,IAAA,IAO1B,EAAO,CACT,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CAGjB,IAAM,EACJ,IAAmB,IAAA,IAAa,IAAsB,IAAA,GAExD,IAAI,IAAc,SAAW,KAC3B,EAAe,EAAQ,EAAc,GAAG,CAAI,GAG1C,MAAmB,IAAA,IAAa,CAAC,GAMrC,IACE,IAAqB,IAAA,IACrB,IAAuB,IAAA,IACvB,IAAa,IAAA,GACb,CAIA,aAAa,CAAc,EAE3B,IAAM,EAAM,KAAK,IAAI,EAErB,IAAwB,EAExB,IAAM,EACJ,IAAuB,IAAA,GAClB,GAAoB,EACrB,KAAK,IACH,GAAoB,EAGpB,KAAK,IAAI,EAAG,GAAsB,EAAM,EAAoB,CAC9D,EAEN,EAAiB,WAAW,EAAgB,CAAgB,CAC9D,CAEI,IAAc,OAAS,GACzB,EAAO,CAHT,CAKF,EAEA,WAAc,CACZ,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAEtB,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEpB,EAAe,IAAA,EACjB,EAEA,UAAa,CACX,EAAe,EACf,EAAkB,CACpB,EAEA,IAAI,QAAS,CACX,OAAO,IAAmB,IAAA,IAAa,IAAsB,IAAA,EAC/D,CACF,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"funnel.js","names":[],"sources":["../src/funnel.ts"],"sourcesContent":["import type { RequireAtLeastOne } from \"type-fest\";\n\n// We use the value provided by the reducer to also determine if a call\n// was done during a timeout period. This means that even when no reducer\n// is provided, we still need a dummy reducer that would return something\n// other than `undefined`. It is safe to cast this to R (which might be\n// anything) because the callback would never use it as it would be typed\n// as a zero-args function.\nconst VOID_REDUCER_SYMBOL = Symbol(\"funnel/voidReducer\");\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- Intentional, so that it could be used as a default value above.\nconst voidReducer = <R>(): R => VOID_REDUCER_SYMBOL as R;\n\ntype FunnelOptions<Args extends RestArguments, R> = {\n readonly reducer?: (accumulator: R | undefined, ...params: Args) => R;\n} & FunnelTimingOptions;\n\n// Not all combinations of timing options are valid, there are dependencies\n// between them to ensure users can't configure the funnel in a way which would\n// cause it to never trigger.\ntype FunnelTimingOptions =\n | ({ readonly triggerAt?: \"end\" } & (\n | ({ readonly minGapMs: number } & RequireAtLeastOne<{\n readonly minQuietPeriodMs: number;\n readonly maxBurstDurationMs: number;\n }>)\n | {\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: never;\n }\n ))\n | {\n readonly triggerAt: \"start\" | \"both\";\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: number;\n };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeScript has some quirks with generic function types, and works best with `any` and not `unknown`. This follows the typing of built-in utilities like `ReturnType` and `Parameters`.\ntype RestArguments = any[];\n\ntype Funnel<Args extends RestArguments = []> = {\n /**\n * Call the function. This might result in the `execute` function being called\n * now or later, depending on it's configuration and it's current state.\n *\n * @param args - The args are defined by the `reducer` function.\n */\n\n readonly call: (...args: Args) => void;\n\n /**\n * Resets the funnel to it's initial state. Any calls made since the last\n * invocation will be discarded.\n */\n readonly cancel: () => void;\n\n /**\n * Triggers an invocation regardless of the current state of the funnel.\n * Like any other invocation, The funnel will also be reset to it's initial\n * state afterwards.\n */\n readonly flush: () => void;\n\n /**\n * The funnel is in it's initial state (there are no active timeouts).\n */\n readonly isIdle: boolean;\n};\n\n/**\n * Creates a funnel that controls the timing and execution of `callback`. Its\n * main purpose is to manage multiple consecutive (usually fast-paced) calls,\n * reshaping them according to a defined batching strategy and timing policy.\n * This is useful when handling uncontrolled call rates, such as DOM events or\n * network traffic. It can implement strategies like debouncing, throttling,\n * batching, and more.\n *\n * An optional `reducer` function can be provided to allow passing data to the\n * callback via calls to `call` (otherwise the signature of `call` takes no\n * arguments).\n *\n * Typing is inferred from `callback`s param, and from the rest params that\n * the optional `reducer` function accepts. Use **explicit** types for these\n * to ensure that everything _else_ is well-typed.\n *\n * Notice that this function constructs a funnel **object**, and does **not**\n * execute anything when called. The returned object should be used to execute\n * the funnel via the its `call` method.\n *\n * - Debouncing: use `minQuietPeriodMs` and any `triggerAt`.\n * - Throttling: use `minGapMs` and `triggerAt: \"start\"` or `\"both\"`.\n * - Batching: See the reference implementation in [`funnel.reference-batch.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.reference-batch.test.ts).\n *\n * @param callback - The main function that would be invoked periodically based\n * on `options`. The function would take the latest result of the `reducer`; if\n * no calls where made since the last time it was invoked it will not be\n * invoked. (If a return value is needed, it should be passed via a reference or\n * via closure to the outer scope of the funnel).\n * @param options - An object that defines when `execute` should be invoked,\n * relative to the calls of `call`. An empty/missing options object is\n * equivalent to setting `minQuietPeriodMs` to `0`.\n * @param options.reducer - Combines the arguments passed to `call` with the\n * value computed on the previous call (or `undefined` on the first time). The\n * goal of the function is to extract and summarize the data needed for\n * `callback`. It should be fast and simple as it is called often and should\n * defer heavy operations to the `execute` function. If the final value\n * is `undefined`, `callback` will not be called.\n * @param options.triggerAt - At what \"edges\" of the funnel's burst window\n * would `execute` invoke:\n * - `start` - the function will be invoked immediately (within the **same**\n * execution frame!), and any subsequent calls would be ignored until the funnel\n * is idle again. During this period `reducer` will also not be called.\n * - `end` - the function will **not** be invoked initially but the timer will\n * be started. Any calls during this time would be passed to the reducer, and\n * when the timers are done, the reduced result would trigger an invocation.\n * - `both` - the function will be invoked immediately, and then the funnel\n * would behave as if it was in the 'end' state. Default: 'end'.\n * @param options.minQuietPeriodMs - The burst timer prevents subsequent calls\n * in short succession to cause excessive invocations (aka \"debounce\"). This\n * duration represents the **minimum** amount of time that needs to pass\n * between calls (the \"quiet\" part) in order for the subsequent call to **not**\n * be considered part of the burst. In other words, as long as calls are faster\n * than this, they are considered part of the burst and the burst is extended.\n * @param options.maxBurstDurationMs - Bursts are extended every time a call is\n * made within the burst period. This means that the burst period could be\n * extended indefinitely. To prevent such cases, a maximum burst duration could\n * be defined. When `minQuietPeriodMs` is not defined and this option is, they\n * will both share the same value.\n * @param options.minGapMs - A minimum duration between calls of `execute`.\n * This is maintained regardless of the shape of the burst and is ensured even\n * if the `maxBurstDurationMs` is reached before it. (aka \"throttle\").\n * @returns A funnel with a `call` function that is used to trigger invocations.\n * In addition to it the funnel also comes with the following functions and\n * properties:\n * - `cancel` - Resets the funnel to it's initial state, discarding the current\n * `reducer` result without calling `execute` on it.\n * - `flush` - Triggers an invocation even if there are active timeouts, and\n * then resets the funnel to it's initial state.\n * - `isIdle` - Checks if there are any active timeouts.\n * @signature\n * funnel(callback, options);\n * @example\n * const debouncer = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minQuietPeriodMs: 100 },\n * );\n * debouncer.call();\n * debouncer.call();\n *\n * const throttle = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minGapMs: 100, triggerAt: \"start\" },\n * );\n * throttle.call();\n * throttle.call();\n * @category Function\n */\nexport function funnel<Args extends RestArguments = [], R = never>(\n callback: (data: R) => void,\n {\n triggerAt = \"end\",\n minQuietPeriodMs,\n maxBurstDurationMs,\n minGapMs,\n reducer = voidReducer,\n }: FunnelOptions<Args, R>,\n): Funnel<Args> {\n // We manage execution via 2 timeouts, one to track bursts of calls, and one\n // to track the interval between invocations. Together we refer to the period\n // where any of these are active as a \"cool-down period\".\n let burstTimeoutId: ReturnType<typeof setTimeout> | undefined;\n let intervalTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Until invoked, all calls are reduced into a single value that would be sent\n // to the executor on invocation.\n let preparedData: R | undefined;\n\n // In order to be able to limit the total size of the burst (when\n // `maxBurstDurationMs` is used) we need to track when the burst started.\n let burstStartTimestamp: number | undefined;\n\n const invoke = (): void => {\n const param = preparedData;\n if (param === undefined) {\n // There were no calls during both cool-down periods.\n return;\n }\n\n // Make sure the args aren't accidentally used again\n preparedData = undefined;\n\n if (param === VOID_REDUCER_SYMBOL) {\n // @ts-expect-error [ts2554] -- R is typed as `never` because we hide the\n // symbol that `voidReducer` returns; there's no way to make TypeScript\n // aware of this.\n callback();\n } else {\n callback(param);\n }\n\n if (minGapMs !== undefined) {\n intervalTimeoutId = setTimeout(handleIntervalEnd, minGapMs);\n }\n };\n\n const handleIntervalEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n if (burstTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n const handleBurstEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n if (intervalTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n return {\n call: (...args) => {\n // We act based on the initial state of the timeouts before the call is\n // handled and causes the timeouts to change.\n const wasIdle =\n burstTimeoutId === undefined && intervalTimeoutId === undefined;\n\n if (triggerAt !== \"start\" || wasIdle) {\n preparedData = reducer(preparedData, ...args);\n }\n\n if (burstTimeoutId === undefined && !wasIdle) {\n // We are not in an active burst period but in an interval period. We\n // don't start a new burst window until the next invoke.\n return;\n }\n\n if (\n minQuietPeriodMs !== undefined ||\n maxBurstDurationMs !== undefined ||\n minGapMs === undefined\n ) {\n // The timeout tracking the burst period needs to be reset every time\n // another call is made so that it waits the full cool-down duration\n // before it is released.\n clearTimeout(burstTimeoutId);\n\n const now = Date.now();\n\n burstStartTimestamp ??= now;\n\n const burstRemainingMs =\n maxBurstDurationMs === undefined\n ? (minQuietPeriodMs ?? 0)\n : Math.min(\n minQuietPeriodMs ?? maxBurstDurationMs,\n // We need to account for the time already spent so that we\n // don't wait longer than the maxDelay.\n Math.max(0, maxBurstDurationMs - (now - burstStartTimestamp)),\n );\n\n burstTimeoutId = setTimeout(handleBurstEnd, burstRemainingMs);\n }\n\n if (triggerAt !== \"end\" && wasIdle) {\n invoke();\n }\n },\n\n cancel: () => {\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n preparedData = undefined;\n },\n\n flush: () => {\n handleBurstEnd();\n handleIntervalEnd();\n },\n\n get isIdle() {\n return burstTimeoutId === undefined && intervalTimeoutId === undefined;\n },\n };\n}\n"],"mappings":"AAQA,MAAM,EAAsB,OAAO,oBAAoB,EAEjD,MAA0B,EAwJhC,SAAgB,EACd,EACA,CACE,YAAY,MACZ,mBACA,qBACA,WACA,UAAU,GAEE,CAId,IAAI,EACA,EAIA,EAIA,EAEE,MAAqB,CACzB,IAAM,EAAQ,EACV,IAAU,IAAA,KAMd,EAAe,IAAA,GAEX,IAAU,EAIZ,EAAS,EAET,EAAS,CAAK,EAGZ,IAAa,IAAA,KACf,EAAoB,WAAW,EAAmB,CAAQ,GAE9D,EAEM,MAAgC,CAGpC,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEhB,IAAmB,IAAA,IAOvB,EAAO,CACT,EAEM,MAA6B,CAGjC,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAElB,IAAsB,IAAA,IAO1B,EAAO,CACT,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CAGjB,IAAM,EACJ,IAAmB,IAAA,IAAa,IAAsB,IAAA,GAExD,IAAI,IAAc,SAAW,KAC3B,EAAe,EAAQ,EAAc,GAAG,CAAI,GAG1C,MAAmB,IAAA,IAAa,CAAC,GAMrC,IACE,IAAqB,IAAA,IACrB,IAAuB,IAAA,IACvB,IAAa,IAAA,GACb,CAIA,aAAa,CAAc,EAE3B,IAAM,EAAM,KAAK,IAAI,EAErB,IAAwB,EAExB,IAAM,EACJ,IAAuB,IAAA,GAClB,GAAoB,EACrB,KAAK,IACH,GAAoB,EAGpB,KAAK,IAAI,EAAG,GAAsB,EAAM,EAAoB,CAC9D,EAEN,EAAiB,WAAW,EAAgB,CAAgB,CAC9D,CAEI,IAAc,OAAS,GACzB,EAAO,CAHT,CAKF,EAEA,WAAc,CACZ,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAEtB,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEpB,EAAe,IAAA,EACjB,EAEA,UAAa,CACX,EAAe,EACf,EAAkB,CACpB,EAEA,IAAI,QAAS,CACX,OAAO,IAAmB,IAAA,IAAa,IAAsB,IAAA,EAC/D,CACF,CACF"}
1
+ {"version":3,"file":"funnel.js","names":[],"sources":["../src/funnel.ts"],"sourcesContent":["import type { RequireAtLeastOne } from \"type-fest\";\n\n// We use the value provided by the reducer to also determine if a call\n// was done during a timeout period. This means that even when no reducer\n// is provided, we still need a dummy reducer that would return something\n// other than `undefined`. It is safe to cast this to R (which might be\n// anything) because the callback would never use it as it would be typed\n// as a zero-args function.\nconst VOID_REDUCER_SYMBOL = Symbol(\"funnel/voidReducer\");\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- Intentional, so that it could be used as a default value above.\nconst voidReducer = <R>(): R => VOID_REDUCER_SYMBOL as R;\n\ntype FunnelOptions<Args extends RestArguments, R> = {\n readonly reducer?: (accumulator: R | undefined, ...params: Args) => R;\n} & FunnelTimingOptions;\n\n// Not all combinations of timing options are valid, there are dependencies\n// between them to ensure users can't configure the funnel in a way which would\n// cause it to never trigger.\ntype FunnelTimingOptions =\n | ({ readonly triggerAt?: \"end\" } & (\n | ({ readonly minGapMs: number } & RequireAtLeastOne<{\n readonly minQuietPeriodMs: number;\n readonly maxBurstDurationMs: number;\n }>)\n | {\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: never;\n }\n ))\n | {\n readonly triggerAt: \"start\" | \"both\";\n readonly minQuietPeriodMs?: number;\n readonly maxBurstDurationMs?: number;\n readonly minGapMs?: number;\n };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeScript has some quirks with generic function types, and works best with `any` and not `unknown`. This follows the typing of built-in utilities like `ReturnType` and `Parameters`.\ntype RestArguments = any[];\n\ntype Funnel<Args extends RestArguments = []> = {\n /**\n * Call the function. This might result in the `execute` function being called\n * now or later, depending on it's configuration and it's current state.\n *\n * @param args - The args are defined by the `reducer` function.\n */\n\n readonly call: (...args: Args) => void;\n\n /**\n * Resets the funnel to it's initial state. Any calls made since the last\n * invocation will be discarded.\n */\n readonly cancel: () => void;\n\n /**\n * Triggers an invocation regardless of the current state of the funnel.\n * Like any other invocation, The funnel will also be reset to it's initial\n * state afterwards.\n */\n readonly flush: () => void;\n\n /**\n * The funnel is in it's initial state (there are no active timeouts).\n */\n readonly isIdle: boolean;\n};\n\n/**\n * Creates a funnel that controls the timing and execution of `callback`. Its\n * main purpose is to manage multiple consecutive (usually fast-paced) calls,\n * reshaping them according to a defined batching strategy and timing policy.\n * This is useful when handling uncontrolled call rates, such as DOM events or\n * network traffic. It can implement strategies like debouncing, throttling,\n * batching, and more.\n *\n * An optional `reducer` function can be provided to allow passing data to the\n * callback via calls to `call` (otherwise the signature of `call` takes no\n * arguments).\n *\n * Typing is inferred from `callback`s param, and from the rest params that\n * the optional `reducer` function accepts. Use **explicit** types for these\n * to ensure that everything _else_ is well-typed.\n *\n * Notice that this function constructs a funnel **object**, and does **not**\n * execute anything when called. The returned object should be used to execute\n * the funnel via the its `call` method.\n *\n * The type of the funnel object is not exported; when you need to reference\n * it explicitly (e.g., a class property holding a funnel) use\n * `ReturnType<typeof funnel<[]>>`, replacing `[]` with the `reducer`'s rest\n * params when one is used (e.g., `ReturnType<typeof funnel<[string]>>`).\n *\n * - Debouncing: use `minQuietPeriodMs` and any `triggerAt`. Copy-paste\n * reference implementations are available for Remeda's deprecated `debounce`\n * function in [`funnel.remeda-debounce.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts),\n * and for the Lodash `debounce` function in the [Lodash migration docs](https://remedajs.com/migrate/lodash#debounce).\n * - Throttling: use `minGapMs` and `triggerAt: \"start\"` or `\"both\"`.\n * Copy-paste reference implementations for the Lodash `throttle` function\n * (which maps onto the burst options `minQuietPeriodMs` and\n * `maxBurstDurationMs` instead of `minGapMs`) are available in the\n * [Lodash migration docs](https://remedajs.com/migrate/lodash#throttle).\n * - Batching: a copy-paste reference implementation is available in [`funnel.reference-batch.test.ts`](https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.reference-batch.test.ts).\n *\n * @param callback - The main function that would be invoked periodically based\n * on `options`. The function would take the latest result of the `reducer`; if\n * no calls where made since the last time it was invoked it will not be\n * invoked. (If a return value is needed, it should be passed via a reference or\n * via closure to the outer scope of the funnel).\n * @param options - An object that defines when `execute` should be invoked,\n * relative to the calls of `call`. An empty/missing options object is\n * equivalent to setting `minQuietPeriodMs` to `0`.\n * @param options.reducer - Combines the arguments passed to `call` with the\n * value computed on the previous call (or `undefined` on the first time). The\n * goal of the function is to extract and summarize the data needed for\n * `callback`. It should be fast and simple as it is called often and should\n * defer heavy operations to the `execute` function. If the final value\n * is `undefined`, `callback` will not be called.\n * @param options.triggerAt - At what \"edges\" of the funnel's burst window\n * would `execute` invoke:\n * - `start` - the function will be invoked immediately (within the **same**\n * execution frame!), and any subsequent calls would be ignored until the funnel\n * is idle again. During this period `reducer` will also not be called.\n * - `end` - the function will **not** be invoked initially but the timer will\n * be started. Any calls during this time would be passed to the reducer, and\n * when the timers are done, the reduced result would trigger an invocation.\n * - `both` - the function will be invoked immediately, and then the funnel\n * would behave as if it was in the 'end' state. Default: 'end'.\n * @param options.minQuietPeriodMs - The burst timer prevents subsequent calls\n * in short succession to cause excessive invocations (aka \"debounce\"). This\n * duration represents the **minimum** amount of time that needs to pass\n * between calls (the \"quiet\" part) in order for the subsequent call to **not**\n * be considered part of the burst. In other words, as long as calls are faster\n * than this, they are considered part of the burst and the burst is extended.\n * @param options.maxBurstDurationMs - Bursts are extended every time a call is\n * made within the burst period. This means that the burst period could be\n * extended indefinitely. To prevent such cases, a maximum burst duration could\n * be defined. When `minQuietPeriodMs` is not defined and this option is, they\n * will both share the same value.\n * @param options.minGapMs - A minimum duration between calls of `execute`.\n * This is maintained regardless of the shape of the burst and is ensured even\n * if the `maxBurstDurationMs` is reached before it. (aka \"throttle\").\n * @returns A funnel with a `call` function that is used to trigger invocations.\n * In addition to it the funnel also comes with the following functions and\n * properties:\n * - `cancel` - Resets the funnel to it's initial state, discarding the current\n * `reducer` result without calling `execute` on it.\n * - `flush` - Triggers an invocation even if there are active timeouts, and\n * then resets the funnel to it's initial state.\n * - `isIdle` - Checks if there are any active timeouts.\n * @signature\n * funnel(callback, options);\n * @example\n * const debouncer = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minQuietPeriodMs: 100 },\n * );\n * debouncer.call();\n * debouncer.call();\n *\n * const throttle = funnel(\n * () => {\n * console.log(\"Callback executed!\");\n * },\n * { minGapMs: 100, triggerAt: \"start\" },\n * );\n * throttle.call();\n * throttle.call();\n * @category Function\n */\nexport function funnel<Args extends RestArguments = [], R = never>(\n callback: (data: R) => void,\n {\n triggerAt = \"end\",\n minQuietPeriodMs,\n maxBurstDurationMs,\n minGapMs,\n reducer = voidReducer,\n }: FunnelOptions<Args, R>,\n): Funnel<Args> {\n // We manage execution via 2 timeouts, one to track bursts of calls, and one\n // to track the interval between invocations. Together we refer to the period\n // where any of these are active as a \"cool-down period\".\n let burstTimeoutId: ReturnType<typeof setTimeout> | undefined;\n let intervalTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Until invoked, all calls are reduced into a single value that would be sent\n // to the executor on invocation.\n let preparedData: R | undefined;\n\n // In order to be able to limit the total size of the burst (when\n // `maxBurstDurationMs` is used) we need to track when the burst started.\n let burstStartTimestamp: number | undefined;\n\n const invoke = (): void => {\n const param = preparedData;\n if (param === undefined) {\n // There were no calls during both cool-down periods.\n return;\n }\n\n // Make sure the args aren't accidentally used again\n preparedData = undefined;\n\n if (param === VOID_REDUCER_SYMBOL) {\n // @ts-expect-error [ts2554] -- R is typed as `never` because we hide the\n // symbol that `voidReducer` returns; there's no way to make TypeScript\n // aware of this.\n callback();\n } else {\n callback(param);\n }\n\n if (minGapMs !== undefined) {\n intervalTimeoutId = setTimeout(handleIntervalEnd, minGapMs);\n }\n };\n\n const handleIntervalEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n if (burstTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n const handleBurstEnd = (): void => {\n // When called via a timeout the timeout is already cleared, but when called\n // via `flush` we need to manually clear it.\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n if (intervalTimeoutId !== undefined) {\n // As long as one of the timeouts is active we don't invoke the function.\n // Each timeout's end event handler has a call to invoke, so we are\n // guaranteed to invoke the function eventually.\n return;\n }\n\n invoke();\n };\n\n return {\n call: (...args) => {\n // We act based on the initial state of the timeouts before the call is\n // handled and causes the timeouts to change.\n const wasIdle =\n burstTimeoutId === undefined && intervalTimeoutId === undefined;\n\n if (triggerAt !== \"start\" || wasIdle) {\n preparedData = reducer(preparedData, ...args);\n }\n\n if (burstTimeoutId === undefined && !wasIdle) {\n // We are not in an active burst period but in an interval period. We\n // don't start a new burst window until the next invoke.\n return;\n }\n\n if (\n minQuietPeriodMs !== undefined ||\n maxBurstDurationMs !== undefined ||\n minGapMs === undefined\n ) {\n // The timeout tracking the burst period needs to be reset every time\n // another call is made so that it waits the full cool-down duration\n // before it is released.\n clearTimeout(burstTimeoutId);\n\n const now = Date.now();\n\n burstStartTimestamp ??= now;\n\n const burstRemainingMs =\n maxBurstDurationMs === undefined\n ? (minQuietPeriodMs ?? 0)\n : Math.min(\n minQuietPeriodMs ?? maxBurstDurationMs,\n // We need to account for the time already spent so that we\n // don't wait longer than the maxDelay.\n Math.max(0, maxBurstDurationMs - (now - burstStartTimestamp)),\n );\n\n burstTimeoutId = setTimeout(handleBurstEnd, burstRemainingMs);\n }\n\n if (triggerAt !== \"end\" && wasIdle) {\n invoke();\n }\n },\n\n cancel: () => {\n clearTimeout(burstTimeoutId);\n burstTimeoutId = undefined;\n burstStartTimestamp = undefined;\n\n clearTimeout(intervalTimeoutId);\n intervalTimeoutId = undefined;\n\n preparedData = undefined;\n },\n\n flush: () => {\n handleBurstEnd();\n handleIntervalEnd();\n },\n\n get isIdle() {\n return burstTimeoutId === undefined && intervalTimeoutId === undefined;\n },\n };\n}\n"],"mappings":"AAQA,MAAM,EAAsB,OAAO,oBAAoB,EAEjD,MAA0B,EAoKhC,SAAgB,EACd,EACA,CACE,YAAY,MACZ,mBACA,qBACA,WACA,UAAU,GAEE,CAId,IAAI,EACA,EAIA,EAIA,EAEE,MAAqB,CACzB,IAAM,EAAQ,EACV,IAAU,IAAA,KAMd,EAAe,IAAA,GAEX,IAAU,EAIZ,EAAS,EAET,EAAS,CAAK,EAGZ,IAAa,IAAA,KACf,EAAoB,WAAW,EAAmB,CAAQ,GAE9D,EAEM,MAAgC,CAGpC,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEhB,IAAmB,IAAA,IAOvB,EAAO,CACT,EAEM,MAA6B,CAGjC,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAElB,IAAsB,IAAA,IAO1B,EAAO,CACT,EAEA,MAAO,CACL,MAAO,GAAG,IAAS,CAGjB,IAAM,EACJ,IAAmB,IAAA,IAAa,IAAsB,IAAA,GAExD,IAAI,IAAc,SAAW,KAC3B,EAAe,EAAQ,EAAc,GAAG,CAAI,GAG1C,MAAmB,IAAA,IAAa,CAAC,GAMrC,IACE,IAAqB,IAAA,IACrB,IAAuB,IAAA,IACvB,IAAa,IAAA,GACb,CAIA,aAAa,CAAc,EAE3B,IAAM,EAAM,KAAK,IAAI,EAErB,IAAwB,EAExB,IAAM,EACJ,IAAuB,IAAA,GAClB,GAAoB,EACrB,KAAK,IACH,GAAoB,EAGpB,KAAK,IAAI,EAAG,GAAsB,EAAM,EAAoB,CAC9D,EAEN,EAAiB,WAAW,EAAgB,CAAgB,CAC9D,CAEI,IAAc,OAAS,GACzB,EAAO,CAHT,CAKF,EAEA,WAAc,CACZ,aAAa,CAAc,EAC3B,EAAiB,IAAA,GACjB,EAAsB,IAAA,GAEtB,aAAa,CAAiB,EAC9B,EAAoB,IAAA,GAEpB,EAAe,IAAA,EACjB,EAEA,UAAa,CACX,EAAe,EACf,EAAkB,CACpB,EAEA,IAAI,QAAS,CACX,OAAO,IAAmB,IAAA,IAAa,IAAsB,IAAA,EAC/D,CACF,CACF"}