docs: function readInternal(stream, size, until, options) {
    const flowing =
      (options && options.flowing) || typeof stream.read !== 'function';
    let numSize =
      size === null || Number.isNaN(Number(size)) ? undefined : Number(size);
    let objectMode = Boolean(options && options.objectMode);
    const timeout = options && options.timeout;
    const ReadPromise = (options && options.Promise)
      || (flowing ? SyncPromise : Promise);
  
    let abortRead;
    let cancelRead;
  
    const promise = new ReadPromise(((resolve, reject, cancelled) => {
      let isDoneReading = false;
      let result = null;
      let timeoutID;
      function doneReading() {
        if (isDoneReading) { return; }
        isDoneReading = true;
        stream.removeListener('data', onData);
        stream.removeListener('end', onEnd);
        stream.removeListener('error', doReject);
        stream.removeListener('readable', readPending);
        if (timeoutID) { clearTimeout(timeoutID); }
      }
      function doReject(err, unshiftResult) {
        doneReading();
  
        if (unshiftResult && result !== null && result.length > 0) {
          result = tryUnshift(stream, result, 0);
        }
  
        if (result !== null) {
          if (typeof err === 'object' && err !== null) {
            // If we have read some data, include it on the error so the caller
            // can use the partial result and to avoid losing data.
            err.read = result;
          } else {
            debug('Unable to set .read on non-object reject cause.  '
              + 'Discarding data.');
          }
        }
        reject(err);
      }
      function doResolve() {
        doneReading();
        resolve(result);
      }
  
      /** Aborts a pending read operation, causing the Promise to be rejected.
       *
       * If the read operation is not currently pending, this does nothing.
       *
       * @function
       * @name CancellableReadPromise#abortRead
       * @see ReadOptions.cancellable
       */
      abortRead = function() {
        if (isDoneReading) { return; }
        doReject(new AbortError('read aborted'), true);
      };
  
      /** Cancels a pending read operation, causing the Promise never to be
       * resolved or rejected.
       *
       * If the read operation is not currently pending, this does nothing.
       *
       * @function
       * @name CancellableReadPromise#cancelRead
       * @return {Buffer|string|Array} Any previously read data which could not
       * be unshifted, or <code>null</code> if all data was unshifted.
       * @see ReadOptions.cancellable
       */
      cancelRead = function() {
        if (isDoneReading) { return null; }
        // Note:  Must stop reading before unshifting to avoid emitting a
        // 'readable' event and immediately re-reading unshifted data.
        doneReading();
        if (result && result.length > 0) {
          result = tryUnshift(stream, result, 0);
        }
        return result;
      };
  
      // bluebird 3.x supports cancellation (when explicitly enabled by the
      // user).  It does not provide a way to query whether it is enabled
      // (AFAICT).  The third argument could be notify or something else.
      // Check for cancel method and config function to add certainty.
      // TODO:  Find a more reliable check.
      if (typeof cancelled === 'function'
          && typeof ReadPromise.prototype.cancel === 'function'
          && typeof ReadPromise.config === 'function') {
        cancelled(cancelRead);
      }
  
      stream.once('error', doReject);
  
      if (timeout !== undefined && timeout !== null) {
        timeoutID = setTimeout(() => {
          doReject(new TimeoutError(), true);
        }, timeout);
      }
  
      /** Calls the until function and handles its result.
       * @return {boolean} <code>true</code> if done reading, <code>false</code>
       * otherwise.
       * @private
       */
      function checkUntil(resultWithData, data, ended) {
        let desiredLength;
        try {
          desiredLength = until(resultWithData, data, ended);
        } catch (errUntil) {
          doReject(errUntil, true);
          return true;
        }
  
        const resultLength = result ? result.length : 0;
        if (typeof desiredLength === 'number') {
          if (desiredLength > resultLength) {
            debug(
              'until returned a desired length of %d.  '
                + 'Only have %d.  Reading up to %d.',
              desiredLength, resultLength, desiredLength
            );
            numSize = desiredLength;
            size = desiredLength - resultLength;
          } else if (desiredLength >= 0) {
            debug(
              'until returned a desired length of %d out of %d',
              desiredLength, resultLength
            );
            if (desiredLength < resultLength) {
              if (ended) {
                debug('Unable to unshift:  Can not unshift after end.');
              } else {
                result = tryUnshift(stream, result, desiredLength, true);
              }
            }
            doResolve();
            return true;
          } else {
            debug('until returned %d, continuing to read', desiredLength);
          }
        } else if (desiredLength === true) {
          debug('until returned true, read finished.');
          doResolve();
          return true;
        } else if (desiredLength !== undefined
            && desiredLength !== null
            && desiredLength !== false) {
          // Note:  Although this could be allowed, it causes an Error so that
          // future versions may add behavior for these values without causing
          // breakage.
          doReject(
            new TypeError(
              `non-numeric, non-boolean until() result: ${desiredLength}`
            ),
            true
          );
        } else {
          debug('until returned %s, continuing to read', desiredLength);
        }
  
        return false;
      }
  
      function onEnd() {
        if (until) {
          if (!checkUntil(result, null, true)) {
            doReject(new EOFError());
          }
        } else {
          doResolve();
        }
      }
      stream.once('end', onEnd);
  
      // Although reading stream internals is distasteful, it is less distasteful
      // than waiting endlessly for data that will never come because the caller
      // was not careful about handling the 'end' event.
      /* eslint-disable no-underscore-dangle */
      if (stream
          && stream._readableState
          && stream._readableState.endEmitted) {
        debug('Error:  stream has ended!  Calling read after end is unreliable!');
        onEnd();
        return;
      }
      /* eslint-enable no-underscore-dangle */
  
      let resultBuf;
      function onData(data) {
        if (result === null) {
          objectMode = objectMode
            || (typeof data !== 'string' && !(data instanceof Buffer));
          result = objectMode ? [data] : data;
        } else if (typeof result === 'string' && typeof data === 'string') {
          result += data;
        } else if (result instanceof Buffer && data instanceof Buffer) {
          // To avoid copying result on every read, make result a slice of
          // resultBuf which grows geometrically as necessary.
          const newResultSize = data.length + result.length;
          if (!resultBuf || newResultSize > resultBuf.length) {
            let newResultBufSize = resultBuf ? resultBuf.length : 128;
            while (newResultBufSize < newResultSize) {
              // Growth factor is a time/space tradeoff.  3/2 seems reasonable.
              // https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
              // Use right-shift for division to avoid unnecessary float+round
              // eslint-disable-next-line no-bitwise
              newResultBufSize = (newResultBufSize * 3) >>> 1;
            }
            resultBuf = Buffer.allocUnsafe
              ? Buffer.allocUnsafe(newResultBufSize)
              // eslint-disable-next-line no-buffer-constructor
              : new Buffer(newResultBufSize);
            result.copy(resultBuf);
          }
          data.copy(resultBuf, result.length);
          result = resultBuf.slice(0, newResultSize);
        } else if (Array.isArray(result)) {
          result.push(data);
        // result/data mismatch, must be in objectMode.  result becomes an Array.
        // This case is really ugly.  Well-behaved streams shouldn't use it.
        } else {
          objectMode = true;
          result = [result, data];
        }
  
        // When doing a single read in objectMode, behave like stream.read(size):
        // ignore size and don't wrap the result in an Array.
        if (objectMode && !until) {
          // Since resolving to null indicates EOF, we do not permit returning
          // null if it was emitted by a 'data' event.  If there is a use-case
          // for this, I'd be open to adding an option to allow it.
          // Note:  Users can use readUntil(stream, untilTrue) to get [null].
          if (result[0] === null) {
            result.shift();
            return;
          }
          if (result.length > 1) {
            result = tryUnshift(stream, result, 1);
          }
          if (result.length === 1) {
            [result] = result;
          }
          doResolve();
          return;
        }
  
        // If we know the target read size, check if it has been reached
        if (numSize) {
          if (result.length >= numSize) {
            if (result.length > numSize) {
              result = tryUnshift(stream, result, numSize);
            }
            doResolve();
          }
          return;
        }
  
        if (!until) {
          doResolve();
          return;
        }
  
        checkUntil(result, data, false);
      }
  
      function readPending() {
        while (!isDoneReading) {
          const data = stream.read(size);
          if (data === null) {
            if (!isDoneReading) {
              stream.once('readable', readPending);
            }
            return;
          }
  
          onData(data);
        }
      }
  
      if (numSize !== undefined && numSize <= 0) {
        // .read(size) will always return null.  Do it once, then done.
        if (typeof stream.read === 'function') {
          result = stream.read(size);
        }
        doResolve();
        return;
      }
  
      if (!flowing) {
        readPending();
      } else {
        stream.on('data', onData);
      }
    }));
  
    if (options && (options.cancellable || options.cancelable)) {
      promise.abortRead = abortRead;
      promise.cancelRead = cancelRead;
    }
  
    return promise;
  }