Можно ли асинхронно собирать элементы из генератора в массив?

Я играю с написанием веб-службы с использованием Node.js/Express, которая генерирует некоторые объекты на основе шаблонов, а затем возвращает сгенерированные данные. Я использую обещания Bluebird для управления всей асинхронной логикой. После удаления всего ненужного мой код выглядит примерно так[1].

Моя проблема заключается в том, что основная логика может заблокироваться на несколько секунд, если запрошенное количество выходных элементов велико. Так как я играл с ES6 для этого проекта, моей первой мыслью было выделить создание элемента в генератор[2]. Однако единственный способ получить все результаты от этого генератора — это Array.from, который не помогает с блокировкой.

Я играл с .map, .all, .coroutine и парой других вещей, пытаясь асинхронно собрать результаты из генератора, но мне не повезло. Есть ли хороший способ сделать это с Bluebird? (Или, возможно, лучший способ сделать это вообще?)

Нативный ES6 Promise.all может принимать итератор и возвращать массив значений, но V8 еще не поддерживает это. Кроме того, в моих экспериментах с polyfills/Firefox он кажется синхронным.

Это не слишком распространенная операция, поэтому меня не волнует абсолютная производительность. Я просто хочу избежать блокировки очереди событий, и я бы предпочел красивое, легко читаемое и поддерживаемое решение.

[1]:

let Bluebird = require('bluebird');

let templates = ...; // logic to load data templates 

function createRandomElementFromRandomTemplate(templates) {
    let el;
    // synchronous work that can take a couple of milliseconds...
    return el;
};

api.createRandomElements = function(req, res) {
    let numEls = req.params.numEls;

    Bluebird.resolve(templates)
    .then(templates => {
        let elements = [];
        // numEls could potentially be several thousand
        for(let i = 0; i < numEls; ++i) {
            elements.push(createRandomElementFromRandomTemplate(templates));
        }
        return elements;
    })
    .then(elements => {
        res.json(elements);
    })
    .error(err => {
        res.status(500).json(err);
    });
}

[2]:

function* generateRandomElementsFromRandomTemplate(templates, numEls) {
    for(let i = 0; i < numEls; ++i) {
        let el;
        // synchronous work that can take a couple of milliseconds...
        yield el;
    }
}

api.createRandomElements = function(req, res) {
    let numEls = req.params.numEls;

    Bluebird.resolve(templates)
    .then(templates => {
        // this still blocks
        return Array.from(generateRandomElementsFromRandomTemplate(templates, numEls));
    })
    .then(elements => {
        res.json(elements);
    })
    .error(err => {
        res.status(500).json(err);
    });
}

person mmebane    schedule 15.01.2015    source источник
comment
Мы рассматривали возможность добавления итератора в Promise.all, но решили, что никто не будет его использовать — пожалуйста, откройте вопрос на GitHub bluebird — это определенно стоит рассмотреть.   -  person Benjamin Gruenbaum    schedule 15.01.2015
comment
В качестве обходного пути — Promise.map принимает второй параметр конфигурации, вы можете передать ему значение параллелизма, которое может решить вашу проблему: см. официальная документация по API   -  person Benjamin Gruenbaum    schedule 15.01.2015
comment
Даже если promise.all поддерживает итерацию, он просто превратит его в массив и будет использовать массив...   -  person Esailija    schedule 16.01.2015
comment
@Esailija да, в общем, поскольку карта не имеет гарантии порядка, поддерживает итерации в карте, и все это не очень интересно (но стоит пересмотреть, если возникнет реальная необходимость) - поддержка их с помощью .each может быть интересной.   -  person Benjamin Gruenbaum    schedule 16.01.2015


Ответы (1)


Вот наполовину приличное решение, которое я нашел после более внимательного изучения .map() Bluebird, как предложил Бенджамин. Однако у меня все еще есть ощущение, что я что-то упускаю.

Основная причина, по которой я начал с Bluebird, заключалась в Mongoose, поэтому я оставил немного этого для более реалистичного образца.

let Bluebird = require('bluebird');
let mongoose = require('mongoose');
Bluebird.promisifyAll(mongoose);

const Template = mongoose.models.Template,
      UserPref = mongoose.models.UserPref;

// just a normal function that generates one element with a random choice of template
function createRandomElementFromRandomTemplate(templates, userPrefs) {
    let el;
    // synchronous work that can take a couple of milliseconds...
    return el;
}

api.generate = function(req, res) {
    let userId = req.params.userId;
    let numRecord = req.params.numRecords
    let data;
    Bluebird.props({
        userprefs: UserPref.findOneAsync({userId: userId}), 
        templates: Template.findAsync({})
    })
    .then(_data => {
        data = _data;
        // use a sparse array to convince .map() to loop the desired number of times
        return Array(numRecords);
    })
    .map(() => {
        // ignore the parameter map passes in - we're using the exact same data in each iteration
        // generate one item each time and let Bluebird collect them into an array
        // I think this could work just as easily with a coroutine
        return Bluebird.delay(createRandomElementFromRandomTemplate(data.templates, data.userprefs), 0);
    }, {concurrency: 5})
    .then(generated => {
        return Generated.createAsync(generated);
    })
    .then(results => {
        res.json(results);
    })
    .catch(err => {
        console.log(err);
        res.status(500);
    });
};
person mmebane    schedule 15.01.2015
comment
Почему бы просто не вернуть new Array(numRecords)? Короче на две строчки - person alexpods; 16.01.2015
comment
Те позволяет - вы случайно не используете 6to5? (или только ио?) - person Benjamin Gruenbaum; 16.01.2015
comment
Ну, я должен признать. Решение @BenjaminGruenbaum выглядит намного, намного, намного красивее, чем мое. - person alexpods; 16.01.2015
comment
@alexpods: О, я и забыл об этом. - person mmebane; 16.01.2015
comment
@BenjaminGruenbaum: Да, от 6 до 5. Кажется, пока это работает достаточно хорошо. - person mmebane; 16.01.2015
comment
Хорошо, я удаляю свое неудачное предложение. @BenjaminGruenbaum, приятно было с тобой поспорить - person alexpods; 16.01.2015
comment
@alexpods в любое время, пожалуйста, примите участие в теге :) mmebane - мне тоже хорошо работает :) Сообщаю вам, что вы можете использовать асинхронные функции и другие вкусности для многих из этих вещей, 6to5 даже имеет флаг bluebirdCoroutine. - person Benjamin Gruenbaum; 16.01.2015
comment
@BenjaminGruenbaum: я немного изучил это, но мне понадобится некоторое время, чтобы полностью осознать асинхронные функции. :) - person mmebane; 16.01.2015