function createFontResolver(fontParser, unicodeFontResolverClient) {
  /**
   * @type {Record<string, ParsedFont>}
   */
  const parsedFonts = Object.create(null)

  /**
   * @type {Record<string, Array<(ParsedFont) => void>>}
   */
  const loadingFonts = Object.create(null)

  /**
   * Load a given font url
   */
  function doLoadFont(url, callback) {
    const onError = err => {
      console.error(`Failure loading font ${url}`, err)
    }
    try {
      const request = new XMLHttpRequest()
      request.open('get', url, true)
      request.responseType = 'arraybuffer'
      request.onload = function () {
        if (request.status >= 400) {
          onError(new Error(request.statusText))
        }
        else if (request.status > 0) {
          try {
            const fontObj = fontParser(request.response)
            fontObj.src = url;
            callback(fontObj)
          } catch (e) {
            onError(e)
          }
        }
      }
      request.onerror = onError
      request.send()
    } catch(err) {
      onError(err)
    }
  }


  /**
   * Load a given font url if needed, invoking a callback when it's loaded. If already
   * loaded, the callback will be called synchronously.
   * @param {string} fontUrl
   * @param {(font: ParsedFont) => void} callback
   */
  function loadFont(fontUrl, callback) {
    let font = parsedFonts[fontUrl]
    if (font) {
      callback(font)
    } else if (loadingFonts[fontUrl]) {
      loadingFonts[fontUrl].push(callback)
    } else {
      loadingFonts[fontUrl] = [callback]
      doLoadFont(fontUrl, fontObj => {
        fontObj.src = fontUrl
        parsedFonts[fontUrl] = fontObj
        loadingFonts[fontUrl].forEach(cb => cb(fontObj))
        delete loadingFonts[fontUrl];
      })
    }
  }

  /**
   * For a given string of text, determine which fonts are required to fully render it and
   * ensure those fonts are loaded.
   */
  return function (text, callback, {
    lang,
    fonts: userFonts = [],
    style = 'normal',
    weight = 'normal',
    unicodeFontsURL
  } = {}) {
    const charResolutions = new Uint8Array(text.length);
    const fontResolutions = [];
    if (!text.length) {
      allDone()
    }

    const fontIndices = new Map();
    const fallbackRanges = [] // [[start, end], ...]

    if (style !== 'italic') style = 'normal'
    if (typeof weight !== 'number') {
      weight = weight === 'bold' ? 700 : 400
    }

    if (userFonts && !Array.isArray(userFonts)) {
      userFonts = [userFonts]
    }
    userFonts = userFonts.slice()
      // filter by language
      .filter(def => !def.lang || def.lang.test(lang))
      // switch order for easier iteration
      .reverse()
    if (userFonts.length) {
      const UNKNOWN = 0
      const RESOLVED = 1
      const NEEDS_FALLBACK = 2
      let prevCharResult = UNKNOWN

      ;(function resolveUserFonts (startIndex = 0) {
        for (let i = startIndex, iLen = text.length; i < iLen; i++) {
          const codePoint = text.codePointAt(i)
          // Carry previous character's result forward if:
          // - it resolved to a font that also covers this character
          // - this character is whitespace
          if (
            (prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
            (i > 0 && /\s/.test(text[i]))
          ) {
            charResolutions[i] = charResolutions[i - 1]
            if (prevCharResult === NEEDS_FALLBACK) {
              fallbackRanges[fallbackRanges.length - 1][1] = i
            }
          } else {
            for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
              if (j === jLen) {
                // none of the user fonts matched; needs fallback
                const range = prevCharResult === NEEDS_FALLBACK ?
                  fallbackRanges[fallbackRanges.length - 1] :
                  (fallbackRanges[fallbackRanges.length] = [i, i])
                range[1] = i;
                prevCharResult = NEEDS_FALLBACK;
              } else {
                charResolutions[i] = j;
                const { src, unicodeRange } = userFonts[j];
                // filter by optional explicit unicode ranges
                if (!unicodeRange || isCodeInRanges(codePoint, unicodeRange)) {
                  const fontObj = parsedFonts[src];
                  // font not yet loaded, load it and resume
                  if (!fontObj) {
                    loadFont(src, () => {
                      resolveUserFonts(i);
                    });
                    return;
                  }
                  // if the font actually contains a glyph for this char, lock it in
                  if (fontObj.supportsCodePoint(codePoint)) {
                    let fontIndex = fontIndices.get(fontObj);
                    if (typeof fontIndex !== 'number') {
                      fontIndex = fontResolutions.length;
                      fontResolutions.push(fontObj);
                      fontIndices.set(fontObj, fontIndex);
                    }
                    charResolutions[i] = fontIndex;
                    prevCharResult = RESOLVED;
                    break;
                  }
                }
              }
            }
          }

          if (codePoint > 0xffff && i + 1 < iLen) {
            charResolutions[i + 1] = charResolutions[i]
            i++
            if (prevCharResult === NEEDS_FALLBACK) {
              fallbackRanges[fallbackRanges.length - 1][1] = i
            }
          }
        }
        resolveFallbacks();
      })();
    } else {
      fallbackRanges.push([0, text.length - 1])
      resolveFallbacks();
    }

    function resolveFallbacks() {
      if (fallbackRanges.length) {
        // Combine all fallback substrings into a single string for querying
        const fallbackString = fallbackRanges.map(range => text.substring(range[0], range[1] + 1)).join('\n')
        unicodeFontResolverClient.getFontsForString(fallbackString, {
          lang: lang || undefined,
          style,
          weight,
          dataUrl: unicodeFontsURL
        }).then(({fontUrls, chars}) => {
          // Extract results and put them back in the main array
          const fontIndexOffset = fontResolutions.length
          let charIdx = 0;
          fallbackRanges.forEach(range => {
            for (let i = 0, endIdx = range[1] - range[0]; i <= endIdx; i++) {
              charResolutions[range[0] + i] = chars[charIdx++] + fontIndexOffset
            }
            charIdx++ //skip segment separator
          })

          // Load and parse the fallback fonts - avoiding Promise here to prevent polyfills in the worker
          let loadedCount = 0;
          fontUrls.forEach((url, i) => {
            loadFont(url, fontObj => {
              fontResolutions[i + fontIndexOffset] = fontObj
              if (++loadedCount === fontUrls.length) {
                allDone();
              }
            })
          })
        });
      } else {
        allDone();
      }
    }

    function allDone() {
      callback({
        chars: charResolutions,
        fonts: fontResolutions
      })
    }

    function isCodeInRanges(code, ranges) {
      // todo optimize search - CodePointSet from unicode-font-resolver?
      for (let k = 0; k < ranges.length; k++) {
        const [start, end = start] = ranges[k]
        if (start <= code && code <= end) {
          return true
        }
      }
      return false
    }
  }
}
Factory for the FontResolver function.
@param {FontParser} fontParser
@param {{getFontsForString: function, CodePointSet: function}} unicodeFontResolverClient
@return {FontResolver}