Възможно ли е асинхронно събиране на елементи от генератор в масив?

Играя си с писането на уеб услуга, използвайки 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, но решихме, че никой няма да я използва - моля, отворете проблем в bluebird GitHub - това определено е нещо, което трябва да обмислите.   -  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? (или просто io?) - 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 - работи добре и за мен :) Да ви уведомя, че можете да използвате async функции и други екстри за много от тези неща, 6to5 дори има флаг bluebirdCoroutine. - person Benjamin Gruenbaum; 16.01.2015
comment
@BenjaminGruenbaum: Разгледах го малко, но ще ми отнеме известно време, за да обгърна напълно асинхронните функции. :) - person mmebane; 16.01.2015