{"version":3,"sources":["../src/lib/normalize-options.ts","../src/lib/async-memoize-one.ts","../src/lib/serialization.ts"],"names":["matchIndex"],"mappings":"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","sourcesContent":["import type { FlagOption, GenerousOption } from '../types';\n\nexport function normalizeOptions<T>(\n  flagOptions: GenerousOption<T>[] | undefined,\n): FlagOption<T>[] | undefined {\n  if (!Array.isArray(flagOptions)) return flagOptions;\n\n  return flagOptions.map((option) => {\n    if (typeof option === 'boolean') return { value: option };\n    if (typeof option === 'number') return { value: option };\n    if (typeof option === 'string') return { value: option };\n    if (option === null) return { value: option };\n\n    return option;\n  }) as FlagOption<T>[];\n}\n","// adapted from https://github.com/microlinkhq/async-memoize-one\n// and https://github.com/alexreardon/memoize-one\n\ntype MemoizeOneOptions = {\n  cachePromiseRejection?: boolean;\n};\n\ntype MemoizedFn<TFunc extends (this: any, ...args: any[]) => any> = (\n  this: ThisParameterType<TFunc>,\n  ...args: Parameters<TFunc>\n) => ReturnType<TFunc>;\n\n/**\n * Memoizes an async function, but only keeps the latest result\n */\nexport function memoizeOne<TFunc extends (this: any, ...newArgs: any[]) => any>(\n  fn: TFunc,\n  isEqual: (a: Parameters<TFunc>, b: Parameters<TFunc>) => boolean,\n  { cachePromiseRejection = false }: MemoizeOneOptions = {},\n): MemoizedFn<TFunc> {\n  let calledOnce = false;\n  let oldArgs: Parameters<TFunc>;\n  let lastResult: any;\n\n  function memoized(\n    this: ThisParameterType<TFunc>,\n    ...newArgs: Parameters<TFunc>\n  ) {\n    if (calledOnce && isEqual(newArgs, oldArgs)) return lastResult;\n\n    lastResult = fn.apply(this, newArgs);\n\n    if (!cachePromiseRejection && lastResult.catch) {\n      lastResult.catch(() => (calledOnce = false));\n    }\n\n    calledOnce = true;\n    oldArgs = newArgs;\n\n    return lastResult;\n  }\n\n  return memoized;\n}\n","import type { JsonValue } from '..';\nimport { memoizeOne } from './async-memoize-one';\nimport type { FlagOption } from '../types';\nimport { CompactSign, base64url, compactVerify } from 'jose';\n\n// 252 max options length allows storing index  0 to 251,\n// so 252 is the first SPECIAL_INTEGER\nexport const MAX_OPTION_LENGTH = 252;\n\nenum SPECIAL_INTEGERS {\n  /** Signals that the returned value is not listed in the flag's options */\n  NULL = 252,\n  BOOLEAN_FALSE = 253,\n  BOOLEAN_TRUE = 254,\n  UNLISTED_VALUE = 255,\n}\n\nconst memoizedVerify = memoizeOne(\n  (code: string, secret: string) =>\n    compactVerify(code, base64url.decode(secret), {\n      algorithms: ['HS256'],\n    }),\n  (a, b) => a[0] === b[0] && a[1] === b[1], // only first two args matter\n  { cachePromiseRejection: true },\n);\n\nconst memoizedSign = memoizeOne(\n  (uint8Array: Uint8Array, secret) =>\n    new CompactSign(uint8Array)\n      .setProtectedHeader({ alg: 'HS256' })\n      .sign(base64url.decode(secret)),\n  (a, b) =>\n    // matchedIndices array must be equal\n    a[0].length === b[0].length &&\n    a[0].every((v, i) => b[0][i] === v) &&\n    // secrets must be equal\n    a[1] === b[1],\n  { cachePromiseRejection: true },\n);\n\nfunction splitUint8Array(\n  array: Uint8Array,\n  index: number,\n): [Uint8Array, Uint8Array] {\n  const firstHalf = array.slice(0, index);\n  const secondHalf = array.slice(index);\n  return [firstHalf, secondHalf];\n}\n\n/**\n * Common subset of the flag type used in here\n */\ntype Flag = {\n  key: string;\n  options?: FlagOption<any>[];\n};\n\nexport async function deserialize(\n  code: string,\n  flags: readonly Flag[],\n  secret: string,\n): Promise<Record<string, JsonValue>> {\n  // TODO what happens when verification fails?\n  const { payload } = await memoizedVerify(code, secret);\n\n  const [matchedIndicesArray, valuesUint8Array] =\n    payload.length === flags.length\n      ? [payload]\n      : splitUint8Array(payload, flags.length);\n\n  const valuesArray = valuesUint8Array\n    ? // re-add opening and closing brackets since we remove them when serializing\n      JSON.parse(`[${new TextDecoder().decode(valuesUint8Array)}]`)\n    : null;\n\n  let spilled = 0;\n  return matchedIndicesArray.reduce<Record<string, JsonValue>>(\n    (acc, valueIndex, index) => {\n      const flag = flags[index];\n\n      if (!flag) {\n        throw new Error(`flags: No flag at index ${index}`);\n      }\n\n      switch (valueIndex) {\n        case SPECIAL_INTEGERS.BOOLEAN_FALSE:\n          acc[flag.key] = false;\n          break;\n        case SPECIAL_INTEGERS.BOOLEAN_TRUE:\n          acc[flag.key] = true;\n          break;\n        case SPECIAL_INTEGERS.UNLISTED_VALUE:\n          acc[flag.key] = valuesArray[spilled++];\n          break;\n        case SPECIAL_INTEGERS.NULL:\n          acc[flag.key] = null;\n          break;\n        default:\n          acc[flag.key] = flag.options?.[valueIndex]?.value as JsonValue;\n      }\n\n      return acc;\n    },\n    {},\n  );\n}\n\n/**\n * When serializing flags we find the matching option index for each evaluated value.\n *\n * This means we potentially need to iterate through all options of all flags.\n *\n * When the value we're trying to match is a literal (bool, string, number)\n * we look for it using referntial equality.\n *\n * When the value is an array or object we stringify the value and we stringify\n * the options of each flag and then we search for it by string comparison.\n *\n * This is faster than doing a deep equality check and also allows us not to\n * use any external library.\n *\n * We also cache the result of stringifying all options so in a Map so we only\n * ever need to stringify them once.\n */\nconst matchIndex = (function () {\n  const stringifiedOptionsCache = new Map<FlagOption<any>[], string[]>();\n  return function matchIndex(options: FlagOption<any>[], value: JsonValue) {\n    const t = typeof value;\n\n    // we're looking for a literal value, so we can check using referntial equality\n    if (value === null || t === 'boolean' || t === 'string' || t === 'number') {\n      return options.findIndex((v) => v.value === value);\n    }\n\n    // we're looking for an array or object, so we should check stringified\n    const stringifiedValue = JSON.stringify(value);\n    let stringifiedOptions = stringifiedOptionsCache.get(options);\n    if (!stringifiedOptions) {\n      stringifiedOptions = options.map((o) => JSON.stringify(o.value));\n      stringifiedOptionsCache.set(options, stringifiedOptions);\n    }\n\n    return stringifiedOptions.findIndex(\n      (stringifiedOption) => stringifiedOption === stringifiedValue,\n    );\n  };\n})();\n\nfunction joinUint8Arrays(array1: Uint8Array, array2: Uint8Array): Uint8Array {\n  const combined = new Uint8Array(array1.length + array2.length);\n  combined.set(array1);\n  combined.set(array2, array1.length);\n  return combined;\n}\nexport async function serialize(\n  flagSet: Record<Flag['key'], JsonValue>,\n  flags: readonly Flag[],\n  secret: string,\n) {\n  const unlistedValues: JsonValue[] = [];\n\n  const matchedIndices = new Uint8Array(\n    flags.map((flag) => {\n      const options = Array.isArray(flag.options) ? flag.options : [];\n\n      const value = flagSet[flag.key];\n      if (\n        !Object.prototype.hasOwnProperty.call(flagSet, flag.key) ||\n        value === undefined\n      ) {\n        throw new Error(`flags: Missing value for flag \"${flag.key}\"`);\n      }\n\n      // avoid searching for common values\n      // and ensure they can always be compressed, even if not listed in options\n      switch (value) {\n        case null:\n          return SPECIAL_INTEGERS.NULL;\n        case false:\n          return SPECIAL_INTEGERS.BOOLEAN_FALSE;\n        case true:\n          return SPECIAL_INTEGERS.BOOLEAN_TRUE;\n      }\n\n      const matchedIndex = matchIndex(options, value);\n      if (matchedIndex > -1) return matchedIndex;\n\n      // value was not listed in options, so we need to\n      // transport it using JSON.stringify(). we return 255 to\n      // indicate this value is stringified.\n      // stringified values will be placed at the end of the\n      // indices array\n      unlistedValues.push(value);\n      return SPECIAL_INTEGERS.UNLISTED_VALUE;\n    }),\n  );\n\n  let joined: Uint8Array;\n  // there were unlisted values, so we need to join arrays\n  if (unlistedValues.length > 0) {\n    const jsonArray = new TextEncoder().encode(\n      // slicing removes opening and closing array brackets as they'll always be\n      //  there and we can re-add them when deserializing\n      JSON.stringify(unlistedValues).slice(1, -1),\n    );\n    joined = joinUint8Arrays(matchedIndices, jsonArray);\n  } else {\n    joined = matchedIndices;\n  }\n\n  return memoizedSign(joined, secret);\n}\n"]}