Source: utils/updates.js

/**
 *
 * Generates a hash for the client identifier. Truncates the result
 * to stay within the safe integer range (thx Tarjei). Runs async to
 * support both browser and node contexts.
 * @param identifier
 * @returns {Promise<null|string>}
 */
const getTruncatedHash = async (identifier) => {
  if (typeof window !== 'undefined') {
    const encoder = new TextEncoder()
    const data = encoder.encode(identifier)

    try {
      const hashBuffer = await crypto.subtle.digest('SHA-1', data)
      const hashArray = Array.from(new Uint8Array(hashBuffer))
      return hashArray
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .slice(0, 15)
        .join('')
    } catch (error) {
      console.error('Error calculating hash:', error)
      return null
    }
  } else {
    return getTruncatedHashSync(identifier)
  }
}

/**
 * Only runs in node context, not browser context.
 * @param identifier
 * @returns {string}
 */
const getTruncatedHashSync = identifier => {
  const crypto = require('crypto')
  const hash = crypto.createHash('sha1')
  hash.update(identifier)
  // Extract the first 15 characters (or adjust as needed)
  return hash.digest('hex').slice(0, 15)
}

/**
 * Returns the qualification, bucket the client places in,
 * and the hashed identifier.
 * @param hashedIdentifier
 * @param totalBuckets
 * @param rolloutDirection
 * @param rolloutPercentage
 * @returns {{clientBucket: number, isQualified: boolean, hashedIdentifier}}
 */
const getQualification = ({hashedIdentifier, totalBuckets, rolloutDirection, rolloutPercentage}) => {
  // Calculate the bucket for the client identifier
  const clientBucket = parseInt(hashedIdentifier, 16) % totalBuckets;

  // Determine the qualification threshold based on the rollout direction
  const bucketRolloutThreshold = totalBuckets * (rolloutPercentage / 100)

  // when rolling up, client bucket is compared to a range starting at 0 up
  // to the rollout percentage. when rolling down, client bucket is compared
  // to a range starting from the min (total buckets with the percentage rolled
  // out subtracted from it), up to the max (total buckets)
  const isRollingOutUp = (rolloutDirection === DIRECTION_UP)

  // Check if the client qualifies
  const isQualified = isRollingOutUp
    ? clientBucket <= bucketRolloutThreshold
    : clientBucket > (totalBuckets - bucketRolloutThreshold)

  // Return an object with the qualification result
  return {
    isQualified,
    hashedIdentifier,
    clientBucket
  };
}

/**
 * The default total number of buckets.
 * @type {number}
 */
export const DEFAULT_TOTAL_BUCKETS = 100;

/**
 * The constant representing the 'UP' direction for rollout.
 * @type {string}
 */
export const DIRECTION_UP = 'UP';

/**
 * The constant representing the 'DOWN' direction for rollout.
 * @type {string}
 */
export const DIRECTION_DOWN = 'DOWN';

/**
 *
 * Checks if a client qualifies for rollout based on the provided parameters.
 * @param {Object} options - The options for the rollout check.
 * @param {string} options.clientIdentifier - The client identifier.
 * @param {number} options.rolloutPercentage - The percentage of rollout.
 * @param {string} [options.rolloutDirection=DIRECTION_UP] - The direction of rollout.
 * @param {number} [options.totalBuckets=DEFAULT_TOTAL_BUCKETS] - The total number of buckets.
 * @returns {Object} - An object containing the qualification result.
 * @property {boolean} isQualified - Whether the client qualifies or not.
 * @property {string} hashedIdentifier - The hashed client identifier.
 * @property {number} bucket - The bucket value.
 * @returns {Promise<{clientBucket: number, isQualified: boolean, hashedIdentifier}>}
 */
export const qualifiesForRollout = async ({ clientIdentifier, seed, rolloutPercentage, rolloutDirection = DIRECTION_UP, totalBuckets = DEFAULT_TOTAL_BUCKETS }) => {
  // Hash the client identifier
  const hashedIdentifier = await getTruncatedHash(`${seed?.length > 0 ? `${seed}:` : ''}${clientIdentifier}`);
  return getQualification({
    hashedIdentifier,
    totalBuckets,
    rolloutDirection,
    rolloutPercentage
  })
}

/**
 *
 * Checks if a client qualifies for rollout based on the provided parameters.
 * Performs this synchronously -- only works in node context, not browser context.
 *
 * @param {Object} options - The options for the rollout check.
 * @param {string} options.clientIdentifier - The client identifier.
 * @param {number} options.rolloutPercentage - The percentage of rollout.
 * @param {string} [options.rolloutDirection=DIRECTION_UP] - The direction of rollout.
 * @param {number} [options.totalBuckets=DEFAULT_TOTAL_BUCKETS] - The total number of buckets.
 * @returns {Object} - An object containing the qualification result.
 * @property {boolean} isQualified - Whether the client qualifies or not.
 * @property {string} hashedIdentifier - The hashed client identifier.
 * @property {number} bucket - The bucket value.
 */
export const qualifiesForRolloutSync = ({ clientIdentifier, seed, rolloutPercentage, rolloutDirection = DIRECTION_UP, totalBuckets = DEFAULT_TOTAL_BUCKETS }) => {
  // Hash the client identifier
  const hashedIdentifier = getTruncatedHashSync(`${seed?.length > 0 ? `${seed}:` : ''}${clientIdentifier}`);
  return getQualification({
    hashedIdentifier,
    totalBuckets,
    rolloutDirection,
    rolloutPercentage
  })
}