@ 261106.2
current
'use strict';

const {
  ArrayPrototypePush,
  ArrayPrototypeSlice,
  ObjectDefineProperties,
  PromisePrototypeThen,
  PromiseReject,
  PromiseResolve,
  PromiseWithResolvers,
  ReflectApply,
} = primordials;

const {
  AbortError,
  codes: {
    ERR_THROTTLED,
  },
} = require('internal/errors');
const {
  validateAbortSignal,
  validateBoolean,
  validateFunction,
  validateInteger,
  validateObject,
  validateOneOf,
} = require('internal/validators');
const { addAbortListener } = require('internal/events/abort_listener');
const { kEmptyObject } = require('internal/util');
const { TIMEOUT_MAX } = require('internal/timers');
const { clearTimeout, setTimeout } = require('timers');

const timersBinding = internalBinding('timers');
const { markPromiseAsHandled } = internalBinding('util');

/**
 * @typedef {object} ThrottleOptions
 * @property {number} [concurrency] The maximum number of unsettled invocations.
 * @property {number} [maxPending] The maximum number of queued calls.
 * @property {'queue'|'drop'} [overflow] How calls exceeding the limit are handled.
 * @property {AbortSignal} [signal] An AbortSignal that cancels pending and future calls.
 * @property {boolean} [strict] Whether to enforce the limit over a rolling interval.
 */

/**
 * Creates a function that invokes `fn` at most `limit` times per `interval`.
 * @param {Function} fn
 * @param {number} limit
 * @param {number} interval
 * @param {ThrottleOptions} [options]
 * @returns {Function}
 */
function throttle(fn, limit, interval, options = kEmptyObject) {
  validateFunction(fn, 'fn');
  validateInteger(limit, 'limit', 1);
  validateInteger(interval, 'interval', 0, TIMEOUT_MAX);
  validateObject(options, 'options');

  const {
    concurrency = Infinity,
    maxPending = Infinity,
    overflow = 'queue',
    signal,
    strict = false,
  } = options;

  if (concurrency !== Infinity) {
    validateInteger(concurrency, 'options.concurrency', 1);
  }
  if (maxPending !== Infinity) {
    validateInteger(maxPending, 'options.maxPending', 0);
  }
  validateOneOf(overflow, 'options.overflow', ['queue', 'drop']);
  validateAbortSignal(signal, 'options.signal');
  validateBoolean(strict, 'options.strict');

  if (signal?.aborted) {
    throw new AbortError(undefined, { __proto__: null, cause: signal.reason });
  }

  let abortError;
  let activeCount = 0;
  let pendingCalls = [];
  let pendingIndex = 0;
  let refed = true;
  let strictIndex = 0;
  let strictTicks = [];
  let timeout;
  let windowCount = 0;
  let windowStart;

  function pendingCount() {
    return pendingCalls.length - pendingIndex;
  }

  function dequeue() {
    const call = pendingCalls[pendingIndex];
    pendingCalls[pendingIndex++] = undefined;

    if (pendingIndex === pendingCalls.length) {
      pendingCalls = [];
      pendingIndex = 0;
    } else if (pendingIndex > 1024 && pendingIndex * 2 >= pendingCalls.length) {
      pendingCalls = ArrayPrototypeSlice(pendingCalls, pendingIndex);
      pendingIndex = 0;
    }

    return call;
  }

  function rejectPending(error) {
    const calls = pendingCalls;
    const start = pendingIndex;
    pendingCalls = [];
    pendingIndex = 0;
    for (let i = start; i < calls.length; i++) {
      calls[i].reject(error);
    }
  }

  function resetLimiter() {
    strictIndex = 0;
    strictTicks = [];
    windowCount = 0;
    windowStart = undefined;
  }

  function cancelWithError(error) {
    if (timeout !== undefined) {
      clearTimeout(timeout);
      timeout = undefined;
    }
    resetLimiter();
    rejectPending(error);
  }

  function cancel(reason) {
    const error = reason === undefined ?
      new AbortError() :
      new AbortError(undefined, { __proto__: null, cause: reason });
    cancelWithError(error);
  }

  function abort() {
    if (abortError === undefined) {
      abortError = signal.reason === undefined ?
        new AbortError() :
        new AbortError(undefined, { __proto__: null, cause: signal.reason });
    }
    cancelWithError(abortError);
  }

  function ref() {
    refed = true;
    timeout?.ref();
    return throttled;
  }

  function unref() {
    refed = false;
    timeout?.unref();
    return throttled;
  }

  function hasImmediateCapacity() {
    if (signal?.aborted ||
        pendingCount() !== 0 ||
        activeCount >= concurrency) {
      return false;
    }

    const now = timersBinding.getLibuvNow();
    if (strict) {
      let index = strictIndex;
      while (index < strictTicks.length &&
             now - strictTicks[index] >= interval) {
        index++;
      }
      return strictTicks.length - index < limit;
    }

    return windowStart === undefined ||
      now - windowStart >= interval ||
      windowCount < limit;
  }

  function pruneStrictTicks(now) {
    while (strictIndex < strictTicks.length &&
           now - strictTicks[strictIndex] >= interval) {
      strictIndex++;
    }

    if (strictIndex === strictTicks.length) {
      strictIndex = 0;
      strictTicks = [];
    } else if (strictIndex > 1024 && strictIndex * 2 >= strictTicks.length) {
      strictTicks = ArrayPrototypeSlice(strictTicks, strictIndex);
      strictIndex = 0;
    }
  }

  function hasCapacity(now) {
    if (strict) {
      pruneStrictTicks(now);
      return strictTicks.length - strictIndex < limit;
    }

    if (windowStart === undefined || now - windowStart >= interval) {
      windowCount = 0;
      windowStart = now;
    }
    return windowCount < limit;
  }

  function reserve(now) {
    if (strict) {
      ArrayPrototypePush(strictTicks, now);
    } else {
      windowCount++;
    }
  }

  function onSettled() {
    activeCount--;
    drain();
  }

  function invoke(call) {
    activeCount++;
    let resultPromise;
    try {
      resultPromise = PromiseResolve(ReflectApply(fn, throttled, call.args));
    } catch (error) {
      resultPromise = PromiseReject(error);
    }
    call.resolve(resultPromise);
    PromisePrototypeThen(resultPromise, onSettled, onSettled);
  }

  function drop() {
    const promise = PromiseReject(new ERR_THROTTLED());
    markPromiseAsHandled(promise);
    return promise;
  }

  function schedule() {
    if (timeout !== undefined ||
        pendingCount() === 0 ||
        activeCount >= concurrency) {
      return;
    }

    const now = timersBinding.getLibuvNow();
    if (hasCapacity(now)) {
      drain();
      return;
    }

    const delay = strict ?
      strictTicks[strictIndex] + interval - now :
      windowStart + interval - now;
    timeout = setTimeout(onTimeout, delay);
    if (!refed) timeout.unref();
  }

  function drain() {
    if (signal?.aborted) {
      abort();
      return;
    }

    while (pendingCount() !== 0 && activeCount < concurrency) {
      const now = timersBinding.getLibuvNow();
      if (!hasCapacity(now)) break;
      const call = dequeue();
      reserve(now);
      invoke(call);
    }

    schedule();
  }

  function onTimeout() {
    timeout = undefined;
    drain();
  }

  function throttled(...callArgs) {
    if (signal?.aborted) abort();
    if (abortError !== undefined) return PromiseReject(abortError);

    const now = timersBinding.getLibuvNow();
    const canInvoke = pendingCount() === 0 &&
      activeCount < concurrency &&
      hasCapacity(now);
    if (!canInvoke &&
        (overflow === 'drop' || pendingCount() >= maxPending)) {
      return drop();
    }

    const call = PromiseWithResolvers();
    call.args = callArgs;

    if (canInvoke) {
      reserve(now);
      invoke(call);
    } else {
      ArrayPrototypePush(pendingCalls, call);
      schedule();
    }

    return call.promise;
  }

  function createFn(value) {
    return {
      __proto__: null,
      configurable: true,
      enumerable: true,
      writable: true,
      value,
    };
  }

  ObjectDefineProperties(throttled, {
    __proto__: null,
    cancel: createFn(cancel),
    hasImmediateCapacity: createFn(hasImmediateCapacity),
    ref: createFn(ref),
    unref: createFn(unref),
    pending: {
      __proto__: null,
      enumerable: false,
      get() {
        return pendingCount() === 0 ? null :
          pendingCalls[pendingCalls.length - 1].promise;
      },
    },
    pendingCount: {
      __proto__: null,
      enumerable: false,
      get: pendingCount,
    },
    activeCount: {
      __proto__: null,
      enumerable: false,
      get() {
        return activeCount;
      },
    },
    length: {
      __proto__: null,
      configurable: true,
      value: fn.length,
    },
    name: {
      __proto__: null,
      configurable: true,
      value: fn.name,
    },
  });

  if (signal !== undefined) {
    addAbortListener(signal, abort);
  }

  return throttled;
}

module.exports = throttle;