Node Buffers для выгрузки данных POST запросов

3 минут(ы) чтения

Если контент не отображается, включите VPN.

Как-то раз, за обсуждением Idris ЯП с коллегой, зашла речь про графики plotly. Очередное подделие вызвало у меня неподдельную усмешку. Ещё бы! Что можно ожидать от ещё одного велосипедного чарта, содержащего в себе d3 и webgl? Правильно, валялся бы этот велосипед в бездне гитхаба, если бы у меня получилось установить chartjs-node. Но что-то мой химерный ubuntu-windows терминал, о котором я как раз недавно сделал обзор на моё блоге, не давал пакету установится, выплёвывая свою ругань на node-gyp. Python’ячьи ошибки не забота фронтмэна! Подакдакав, нашёл nodejs враппер plotly, под платный REST-API сервис https://plot.ly/, у которого по нынешним маркетинговым уловкам, конечно же был OpenSource вариант. Он то мне и подошёл.

Прицепив его к своему простенькому боту всё было здорово, пока не вспомнилось что бэкэнд бота лежит на Heroku, а там фишка с fs.createWriteStream() не срабатывает по понятным знатоку SaSS-причинам. Задача вроде плёвая: из результата POST-запроса достать res:

https.request(options, function handleResponse(res) {
  if (res.statusCode !== 200) {
    callback(null, res);
});

Вот только я не знаток днища, и сразу не понял что написанный в доки невнятный imageStream (по-сути res выше) на самом деле является IncomingMessage, и если не следовать доки с их:

var fileStream = fs.createWriteStream('1.png');
imageStream.pipe(fileStream);

Без моих знаний о стримах понимать что происходит было сложно, типичный кейс с Stack Overflow Driven Development не прокатывал, потому что Stackoverflow уже был засран этим нубским говнищем, и искать что-либо нормальное было нереально. Потому залип в нодовские доки, чтобы понять как работают эти их хитрые стримы.

const Writable = stream.Writable;
const ws = Writable();
const buffers = [];
ws._write = (chunk, enc, next) => {
  buffers.push(chunk);
  next();
};
imageStream.pipe(ws);
imageStream.on('end', () => {
  const photoBuffer = Buffer.concat(buffers);
  return bot.sendPhoto(chatId, photoBuffer, {
    caption: 'Photo'
  });
});

Скажу, они работают круто, но сложно. Всякие пайпы, события close, end и разделение на секции Readable и Writable. Итогом стал нижеследующий код, смысл которого в том, чтобы через Writable стрим запайпить тот самый imageStream, данные которого можно будет прочитать в ws._write через чанк. Главная трабла, что привычный механизм добавления чанков через строковую переменную здесь не работает, важно обязательно добавлять невредимый чанк в своем формает Int8Array. Я это сделал через buffers. Затем у imageStream появляется возможность уловить события end в котором складываем все собранные чанки в один буфер и посылаем куда следует. Mission Complete, Boss.

Модуль для работы с Plotly.js

Пример генерации графика с помощью библиотеки Plotly:

myplotly.js

const PLOTLY = {
  LOGIN: 'YOUR_PLOTLY_LOGIN',
  TOKEN: 'YOUR_PLOTLY_PASSWORD'
};

const plotly = require('plotly')(PLOTLY.LOGIN, PLOTLY.TOKEN);
const { Writable } = require('stream');

/**
 * @returns {{x: Array, y: Array, type: string}}
 */
const createTrace = () => {
  /**
   * @constant {string}
   */
  const BAR_TYPE = 'bar';
  return {
    x: [],
    y: [],
    type: BAR_TYPE,
  };
};
/**
 * @returns {{format: string, width: number, height: number}}
 */
const figureOptions = {
  format: 'png',
  width: 768,
  height: 512,
};
/**
 * @param {object} figure - figure
 * @param {object} options - options object
 * @returns {Promise<Buffer>}
 */
const getPlotlyImage = (figure, options = {}) => {
  return new Promise((resolve, reject) => {
    return plotly.getImage(figure, options, (error, imageStream) => {
      if (error) {
        return reject(error);
      }
      return resolve(imageStream);
    });
  });
};
/**
 * @param {object} figure - object figure
 * @param {object} options - options object
 * @returns {Promise<Buffer>}
 */
const getImageBuffer = async (figure, options = {}) => {
  const imageStream = await getPlotlyImage(figure, options);
  return new Promise((resolve, reject) => {
    const ws = Writable();
    const buffers = [];
    ws._write = (chunk, enc, next) => {
      buffers.push(chunk);
      next();
    };
    imageStream.pipe(ws);
    imageStream.on('end', () => {
      const photoBuffer = Buffer.concat(buffers);
      return resolve(photoBuffer);
    });
    imageStream.on('error', (error) => {
      return reject(error);
    });
  });
};
/**
 * @param {string} plotId - plot id
 * @returns {Promise}
 */
const deletePlot = (plotId) => {
  return new Promise((resolve, reject) => {
    return plotly.deletePlot(plotId, (error, plot) => {
      if (error) {
        return reject(error);
      }
      return resolve(plot);
    });
  });
};
/**
 * @param {string|Date} date - date
 * @returns {*}
 */
const getDateString = (date) => {
  if (typeof date === 'string') {
    return date;
  } else {
    return date.toLocaleDateString();
  }
};
/**
 * @param {Array} entryRows - rows
 * @param {Array<string|Date>} rangeTimes - rangeTimes
 * @returns {Promise<Error|Buffer>}
 */
const createPhotoBuffer = async (entryRows, rangeTimes) => {
  const trace = createTrace();
  rangeTimes.forEach((_date) => {
    const traceX = getDateString(_date);
    const traceY = entryRows.filter(({ date }) => {
      return getDateString(date) === traceX;
    }).length;
    const xIndex = trace.x.findIndex((_x) => {
      return _x === traceX;
    });
    if (xIndex < 0) {
      trace.x.push(traceX);
      trace.y.push(traceY);
    } else {
      ++trace.y[xIndex];
    }
  });
  const figure = { data: [trace] };
  console.log('figure', figure)
  const photoBuffer = await getImageBuffer(figure, figureOptions);
  return photoBuffer;
};

module.exports = {
  createPhotoBuffer,
  deletePlot,
};

server.js

Демонстрация работы модуля, генерируем график и отправляем обратно пользователю бинарь с помощью своей функции graph внутри Firebase:

const functions = require('firebase-functions');
const dateFns = require('date-fns');
const { createPhotoBuffer } = require('./myplotly');

const getGraph = async (data) => {
  const rows = data.map(d => {
    // удаляем ненужные данные из rows
    return {
      date: new Date(d.created_at),
    }
  });
  /**
   * @constant
   * @type {number}
   */
  const MS_PER_DAY = 1000 * 60 * 60 * 24;
  /**
   * @param {Date} fromDate - from date
   * @param {Date} untilDate - until date
   * @returns {number}
   */
  function getDifferenceDays (fromDate, untilDate) {
    const differenceDays = Math.floor((untilDate - fromDate) / MS_PER_DAY);
    if (differenceDays < 0) {
      throw new Error('until less from');
    }
    return differenceDays;
  }
  /**
   * Если есть "2016-05-01" и "2016-05-03" то автоматически создаются
   * ["2016-05-01", "2016-05-02", "2016-05-03"]
   *
   * @description Эта функция заполняет датами пустоты во времени
   * @param {string|Date} from - from date
   * @param {string|Date} until - until date
   * @returns {Array}
   */
  function fillRangeTimes (from, until) {
    const fromDate = new Date(from);
    const untilDate = new Date(until);
    if (!(dateFns.isValid(fromDate) && dateFns.isValid(untilDate))) {
      throw new Error('Unknown param type');
    }
    const result = [];
    const dayOffLength = getDifferenceDays(fromDate, untilDate);
    for (let i = 0; i < dayOffLength; i++) {
      const date = new Date(fromDate);
      date.setDate(fromDate.getDate() + i);
      fromDate.setHours(0);
      fromDate.setMinutes(0);
      fromDate.setMinutes(0);
      result.push(date);
    }
    result.push(untilDate);
    return result;
  }
  const firstDate = rows[0].date;
  const latestDate = rows[rows.length - 1].date;
  const rangeTimes = fillRangeTimes(firstDate, latestDate);
  const photoBuffer = await createPhotoBuffer(rows, rangeTimes);

  return photoBuffer;
};

exports.graph = functions.https.onRequest(async (request, response) => {
  const image = await getGraph(request.body);
  response.set('Content-Type', 'image/png');
  response.end(image, 'binary');
});

Приглашаю к общению в комментариях!

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
Предыдущая статья

Как можно вызвать дебаггер обойдя любой линтер

Следующая статья

Стилизуем любые сайты по-своему