{"version":3,"file":"optionValueDecoder.mjs","names":[],"sources":["../../src/optionValueDecoder.ts"],"sourcesContent":["/**\n * This file provides utility functions for determining whether or not an option value combination is present in an encoded option value string.\n *\n * In V1 of the encoding strategy, option value arrays are encoded as a trie with the following rules:\n *  - `:` `,` ` ` and `-` are control characters.\n *  - `:` indicates a new option. ex: 0:1 indicates value 0 for the option in position 1, value 1 for the option in position 2.\n *  - `,` indicates the end of a repeated prefix, mulitple consecutive commas indicate the end of multiple repeated prefixes.\n *  - ` ` indicates a gap in the sequence of option values. ex: `0 4` indicates option values in position 0 and 4 are present.\n *  - `-` indicates a continuous range of option values. ex: `0 1-3 4`. Ranges are only present encoded in the final option value position, so for example the trie for the set [[0,0,0],[0,0,1], ..., [0,2,2]] will be structured as `0:0:0-2,1:0-2,2:0-2`, not `0:0-2:0-2`.\n */\n\nimport {Product} from './storefront-api-types.js';\n\nconst OPTION_VALUE_SEPARATOR = ',';\n\nconst V1_CONTROL_CHARS = {\n  OPTION: ':',\n  END_OF_PREFIX: ',',\n  SEQUENCE_GAP: ' ',\n  RANGE: '-',\n};\n\n/** @publicDocs */\nexport type IsOptionValueCombinationInEncodedVariant = (\n  targetOptionValueCombination: number[],\n  encodedVariantField: string,\n) => boolean;\n\n/**\n * Determine whether an option value combination is present in an encoded option value string. Function is memoized by encodedVariantField.\n *\n * @param targetOptionValueCombination - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing [0] will return true if any option value combination for the first option's option value is present in the encoded string.\n * @param encodedVariantField - Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2026-04/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2026-04/objects/Product#field-encodedvariantavailability)\n * @returns - True if a full or partial targetOptionValueIndices is present in the encoded option value string, false otherwise.\n * @publicDocs\n */\nexport const isOptionValueCombinationInEncodedVariant: IsOptionValueCombinationInEncodedVariant =\n  ((): IsOptionValueCombinationInEncodedVariant => {\n    const decodedOptionValues = new Map<string, Set<string>>();\n\n    return function (\n      targetOptionValueCombination: number[],\n      encodedVariantField: string,\n    ): boolean {\n      if (targetOptionValueCombination.length === 0) {\n        return false;\n      }\n\n      if (!decodedOptionValues.has(encodedVariantField)) {\n        const decodedOptionValuesSet = new Set<string>();\n\n        for (const optionValue of decodeEncodedVariant(encodedVariantField)) {\n          // add the complete option value to the decoded option values set\n          decodedOptionValuesSet.add(optionValue.join(OPTION_VALUE_SEPARATOR));\n\n          // add all composite parts of the option value to the decoded option values set. e.g. if the option value is [0,1,2], add \"0\", \"0,1\", \"0,1,2\"\n          for (let i = 0; i < optionValue.length; i++) {\n            decodedOptionValuesSet.add(\n              optionValue.slice(0, i + 1).join(OPTION_VALUE_SEPARATOR),\n            );\n          }\n        }\n\n        decodedOptionValues.set(encodedVariantField, decodedOptionValuesSet);\n      }\n\n      return Boolean(\n        decodedOptionValues\n          .get(encodedVariantField)\n          ?.has(targetOptionValueCombination.join(OPTION_VALUE_SEPARATOR)),\n      );\n    };\n  })();\n\ntype EncodedVariantField =\n  | Product['encodedVariantAvailability']\n  // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents\n  | Product['encodedVariantExistence'];\ntype DecodedOptionValues = number[][];\n\n/**\n * For an encoded option value string, decode into option value combinations. Entries represent a valid combination formatted as an array of option value positions.\n * @param encodedVariantField - Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2026-04/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2026-04/objects/Product#field-encodedvariantavailability)\n * @returns Decoded option value combinations\n * @publicDocs\n */\nexport function decodeEncodedVariant(\n  encodedVariantField: EncodedVariantField,\n): DecodedOptionValues {\n  if (!encodedVariantField) return [];\n\n  if (encodedVariantField.startsWith('v1_')) {\n    return v1Decoder(stripVersion(encodedVariantField));\n  }\n\n  throw new Error('Unsupported option value encoding');\n}\n\nconst stripVersion: (encodedVariantField: string) => string = (\n  encodedVariantField: string,\n) => encodedVariantField.replace(/^v1_/, '');\n\n/**\n * We encode an array of arrays representing variants, expressed in terms of options and option values, as a trie.\n *\n * This encoding strategy allows extremely large numbers of variants to be expressed in an extremely compact data structure.\n *\n * Integers represent option and values, so [0,0,0] represents option_value at array index 0 for the options at array indexes 0, 1 and 2\n *\n * `:`, `,`, ` ` and `-` are control characters.\n * `:` indicates a new option\n * `,` indicates the end of a repeated prefix, mulitple consecutive commas indicate the end of multiple repeated prefixes.\n * ` ` indicates a gap in the sequence of option values\n * `-` indicates a continuous range of option values\n *\n * Encoding process:\n *\n * example input array: [[0,0,0], [0,1,0], [0,1,1], [1,0,0], [1,0,1], [1,1,1], [2,0,1], [2,1,0]]\n *\n * step 1: encode as string: \"0:0:0,0:1:0,0:1:1,1:0:0,1:0:1,1:1:1,2:0:1,2:1:0,\"\n * step 2: combine nodes that share a prefix: \"0:0:0,0:1:0 1,1:0:0 1,1:1:1,2:0:1,2:1:0,\"\n * step 3: encode data as a trie so no prefixes need to be repeated: \"0:0:0,1:0 1,,1:0:0 1,1:1,,2:0:1,1:0,,\"\n * step 4: since the options are sorted, use a dash to express ranges: \"0:0:0,1:0-1,,1:0:0-1,1:1,,2:0:1,1:0,,\"\n */\nfunction v1Decoder(encodedVariantField: string): number[][] {\n  const tokenizer = /[ :,-]/g;\n  let index = 0;\n  let token: RegExpExecArray | null;\n  const options: number[][] = [];\n  const currentOptionValue: number[] = [];\n  let depth = 0;\n  let rangeStart: number | null = null;\n\n  // iterate over control characters\n  while ((token = tokenizer.exec(encodedVariantField))) {\n    const operation = token[0];\n    const optionValueIndex =\n      Number.parseInt(encodedVariantField.slice(index, token.index)) || 0;\n\n    if (rangeStart !== null) {\n      // If a range has been started, iterate over the range and add each option value to the list of options\n      // - `rangeStart` is set if the last control char was a dash, e.g. `0` for 0-2. It represents the numeric option value position for the start of the range.\n      // - `optionValueIndex` is the numeric option value position for the end of the range\n      for (; rangeStart < optionValueIndex; rangeStart++) {\n        currentOptionValue[depth] = rangeStart;\n        options.push([...currentOptionValue]);\n      }\n      // indicates the range has been processed\n      rangeStart = null;\n    }\n\n    currentOptionValue[depth] = optionValueIndex;\n\n    if (operation === V1_CONTROL_CHARS.RANGE) {\n      // dash operation indicates we are in a range. e.g. 0-2 means option values 0, 1, 2\n      rangeStart = optionValueIndex;\n    } else if (operation === V1_CONTROL_CHARS.OPTION) {\n      // colon operation indicates that we are moving down to the next layer of option values. e.g. 0:0:0-2 means we traverse down from option1 to option3 and represents [[0,0,0], [0,0,1], [0,0,2]]\n      depth++;\n    } else {\n      if (\n        operation === V1_CONTROL_CHARS.SEQUENCE_GAP ||\n        (operation === V1_CONTROL_CHARS.END_OF_PREFIX &&\n          encodedVariantField[token.index - 1] !==\n            V1_CONTROL_CHARS.END_OF_PREFIX)\n      ) {\n        // add the current option value to the list of options if we hit a gap in our sequence or we are at the end of our depth and need to move back up\n        options.push([...currentOptionValue]);\n      }\n      if (operation === V1_CONTROL_CHARS.END_OF_PREFIX) {\n        // go up an option level, trash the last item in currentOptionValue\n        currentOptionValue.pop();\n        depth--;\n      }\n    }\n    index = tokenizer.lastIndex;\n  }\n\n  // The while loop only iterates control characters, meaning if an encoded string ends with an index it will not be processed.\n  const encodingEndsWithIndex = encodedVariantField.match(/\\d+$/g);\n  if (encodingEndsWithIndex) {\n    const finalValueIndex = parseInt(encodingEndsWithIndex[0]);\n    if (rangeStart != null) {\n      // process final range\n      for (; rangeStart <= finalValueIndex; rangeStart++) {\n        currentOptionValue[depth] = rangeStart;\n        options.push([...currentOptionValue]);\n      }\n    } else {\n      // process final index\n      options.push([finalValueIndex]);\n    }\n  }\n\n  return options;\n}\n"],"mappings":";AAaA,IAAM,yBAAyB;AAE/B,IAAM,mBAAmB;CACvB,QAAQ;CACR,eAAe;CACf,cAAc;CACd,OAAO;CACR;;;;;;;;;AAgBD,IAAa,kDACsC;CAC/C,MAAM,sCAAsB,IAAI,KAA0B;AAE1D,QAAO,SACL,8BACA,qBACS;AACT,MAAI,6BAA6B,WAAW,EAC1C,QAAO;AAGT,MAAI,CAAC,oBAAoB,IAAI,oBAAoB,EAAE;GACjD,MAAM,yCAAyB,IAAI,KAAa;AAEhD,QAAK,MAAM,eAAe,qBAAqB,oBAAoB,EAAE;AAEnE,2BAAuB,IAAI,YAAY,KAAK,uBAAuB,CAAC;AAGpE,SAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,IACtC,wBAAuB,IACrB,YAAY,MAAM,GAAG,IAAI,EAAE,CAAC,KAAK,uBAAuB,CACzD;;AAIL,uBAAoB,IAAI,qBAAqB,uBAAuB;;AAGtE,SAAO,QACL,oBACG,IAAI,oBAAoB,EACvB,IAAI,6BAA6B,KAAK,uBAAuB,CAAC,CACnE;;IAED;;;;;;;AAcN,SAAgB,qBACd,qBACqB;AACrB,KAAI,CAAC,oBAAqB,QAAO,EAAE;AAEnC,KAAI,oBAAoB,WAAW,MAAM,CACvC,QAAO,UAAU,aAAa,oBAAoB,CAAC;AAGrD,OAAM,IAAI,MAAM,oCAAoC;;AAGtD,IAAM,gBACJ,wBACG,oBAAoB,QAAQ,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;AAwB5C,SAAS,UAAU,qBAAyC;CAC1D,MAAM,YAAY;CAClB,IAAI,QAAQ;CACZ,IAAI;CACJ,MAAM,UAAsB,EAAE;CAC9B,MAAM,qBAA+B,EAAE;CACvC,IAAI,QAAQ;CACZ,IAAI,aAA4B;AAGhC,QAAQ,QAAQ,UAAU,KAAK,oBAAoB,EAAG;EACpD,MAAM,YAAY,MAAM;EACxB,MAAM,mBACJ,OAAO,SAAS,oBAAoB,MAAM,OAAO,MAAM,MAAM,CAAC,IAAI;AAEpE,MAAI,eAAe,MAAM;AAIvB,UAAO,aAAa,kBAAkB,cAAc;AAClD,uBAAmB,SAAS;AAC5B,YAAQ,KAAK,CAAC,GAAG,mBAAmB,CAAC;;AAGvC,gBAAa;;AAGf,qBAAmB,SAAS;AAE5B,MAAI,cAAc,iBAAiB,MAEjC,cAAa;WACJ,cAAc,iBAAiB,OAExC;OACK;AACL,OACE,cAAc,iBAAiB,gBAC9B,cAAc,iBAAiB,iBAC9B,oBAAoB,MAAM,QAAQ,OAChC,iBAAiB,cAGrB,SAAQ,KAAK,CAAC,GAAG,mBAAmB,CAAC;AAEvC,OAAI,cAAc,iBAAiB,eAAe;AAEhD,uBAAmB,KAAK;AACxB;;;AAGJ,UAAQ,UAAU;;CAIpB,MAAM,wBAAwB,oBAAoB,MAAM,QAAQ;AAChE,KAAI,uBAAuB;EACzB,MAAM,kBAAkB,SAAS,sBAAsB,GAAG;AAC1D,MAAI,cAAc,KAEhB,QAAO,cAAc,iBAAiB,cAAc;AAClD,sBAAmB,SAAS;AAC5B,WAAQ,KAAK,CAAC,GAAG,mBAAmB,CAAC;;MAIvC,SAAQ,KAAK,CAAC,gBAAgB,CAAC;;AAInC,QAAO"}