Синхронное обещание (bluebird против jQuery)

Я разработал небольшую библиотеку для Dynamics CRM REST/ODATA webservice (CrmRestKit). Библиотека зависит от jQuery и использует шаблон обещания, в частности, перспективный шаблон jQuery.

Теперь мне нравится переносить эту библиотеку на bluebird и удалять зависимость jQuery. Но я столкнулся с проблемой, потому что bluebird не поддерживает синхронное разрешение объектов-обещаний.

Некоторая контекстная информация:

API CrmRestKit исключает необязательный параметр, который определяет, должен ли вызов веб-службы выполняться в режиме синхронизации или асинхронного режима:

CrmRestKit.Create( 'Account', { Name: "foobar" }, false ).then( function ( data ) {
   ....
} );

Когда вы передаете "true" или опускаете последний параметр, будет ли метод синхронизировать запись. Режим.

Иногда необходимо выполнить операцию в режиме синхронизации, например, вы можете написать код JavaScript для Dynamics CRM, который был включен для события сохранения формы, и в этом обработчике событий вам необходимо выполнить синхронизацию для проверки (например, подтвердите, что существует определенное количество дочерних записей, в случае наличия правильного количества записей, отмените операцию сохранения и покажите сообщение об ошибке).

Теперь моя проблема заключается в следующем: bluebird не поддерживает разрешение в синхронном режиме. Например, когда я делаю следующее, обработчик "then" вызывается асинхронно:

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

///
/// 'Promise.cast' cast the given value to a trusted promise. 
///
function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return Promise.cast( text );
}

getSomeTextSimpleCast('first').then(print);
print('second');

Вывод следующий:

print -> second
print -> first

Я бы ожидал, что "второе" появится после "первого", потому что обещание уже разрешено со значением. Поэтому я бы предположил, что обработчик then-event немедленно вызывается при применении к уже разрешенному объекту-обещанию.

Когда я делаю то же самое (используйте тогда по уже разрешенному обещанию) с jQuery, у меня будет ожидаемый результат:

function jQueryResolved( opt_text ){

    var text = opt_text || 'jQuery-Test Value',
    dfd =  new $.Deferred();

    dfd.resolve(text);

        // return an already resolved promise
    return dfd.promise();
}

jQueryResolved('third').then(print);
print('fourth');

Это приведет к созданию следующего вывода:

print -> third
print -> fourth

Есть ли способ заставить bluebird работать одинаково?

Update: Предоставленный код был просто для иллюстрации проблемы. Идея lib: Независимо от режима исполнения (sync, async) вызывающий абонент всегда будет обрабатывать объект-обещание.

Относительно "... запроса пользователя... кажется, не имеет никакого смысла": когда вы предоставляете два метода "CreateAsync" и "CreateSync", это также зависит от пользователя, чтобы решить, как выполняется операция,

В любом случае с текущей реализацией поведение по умолчанию (последний параметр является необязательным) - это асинхронное выполнение. Таким образом, для 99% кода требуется объект-обещание, необязательный параметр используется только для 1% случаев, когда вам просто нужно выполнить синхронизацию. Кроме того, я разработал для lib для себя, и я использую в 99,9999% случаев асинхронный режим, но я подумал, что неплохо иметь возможность идти по синхронной дороге, как вам нравится.

Но я думаю, что я понял, что метод синхронизации должен просто вернуть значение. Для следующего выпуска (3.0) я буду использовать "CreateSync" и "CreateAsync".

Спасибо за ваш вклад.

Update-2 Мое намерение для необязательного параметра состояло в том, чтобы обеспечить согласованное поведение и предотвратить логическую ошибку. Предположим, что вы как потребитель моего метода "GetCurrentUserRoles", который использует lib. Таким образом, метод всегда будет давать обещание, это означает, что вам нужно использовать метод "then" для выполнения кода, который зависит от результата. Поэтому, когда кто-то пишет такой код, я согласен, что это абсолютно неправильно:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

if( currentUserRoels.indexOf('foobar') === -1 ){

    // ...
}

Я согласен, что этот код сломается, когда метод GetCurrentUserRoles изменится с синхронизации на асинхронный.

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

Ответ 1

Короткая версия: я понимаю, почему вы хотите это сделать, но ответ - нет.

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

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

Разработчики JavaScript (и многие из комментаторов выше), похоже, придерживаются другой точки зрения, настаивая на том, что если что-то может быть асинхронным, оно всегда должно быть асинхронным. Является ли это "правильным" или нет, не имеет значения - согласно спецификации, которую я нашел в https://promisesaplus.com/, в пункте 2.2.4 указано, что в основном вызовы не могут быть вызваны до тех пор, пока вы не из того, что я буду называть "script code" или "user code"; то есть в спецификации четко указано, что даже если обещание будет завершено, вы не сможете сразу вызвать обратный вызов. Я проверил несколько других мест, и они либо ничего не говорят по этой теме, либо согласны с исходным источником. Я не знаю, можно ли считать https://promisesaplus.com/ окончательным источником информации в этом отношении, но никакие другие источники, которые, как я видел, не соглашались с ним, и, похоже, наиболее полное.

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

Фактический вопрос заключается в том, можно ли настроить Bluebird для выполнения поведения, отличного от JavaScript. Разумеется, для этого может быть небольшая выгода, и в JavaScript все возможно, если вы достаточно стараетесь, но поскольку объект Promise становится все более распространенным на разных платформах, вы увидите переход на его использование как нативного компонента, а не на заказ полиполками или библиотеками. Таким образом, независимо от того, какой ответ сегодня, переработка обещания в Bluebird, вероятно, вызовет у вас проблемы в будущем, и ваш код, вероятно, не должен быть написан, чтобы зависеть или обеспечить немедленное разрешение обещания.

Ответ 2

Точка promises заключается в упрощении асинхронного кода, то есть ближе к тому, что вы чувствуете при использовании синхронного кода.

Вы используете синхронный код. Не делайте это более сложным.

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return text;
}

print(getSomeTextSimpleCast('first'));
print('second');

И это должно быть концом.


Если вы хотите поддерживать один и тот же асинхронный интерфейс, даже если ваш код является синхронным, вам необходимо выполнить все это.

getSomeTextSimpleCast('first')
    .then(print)
    .then(function() { print('second'); });

then получает ваш код из нормального потока выполнения, поскольку он должен быть асинхронным. Bluebird делает это правильно. Простое объяснение того, что он делает:

function then(fn) {
    setTimeout(fn, 0);
}

Обратите внимание, что bluebird на самом деле не делает этого, просто для того, чтобы дать вам простой пример.

Попробуйте!

then(function() {
    console.log('first');
});
console.log('second');

В результате вы получите следующее:

second
first 

Ответ 3

Вы можете подумать, что это проблема, потому что нет способа иметь

getSomeText('first').then(print);
print('second');

и иметь getSomeText "first", напечатанный до "second", когда разрешение синхронно.

Но я думаю, что у вас есть логическая проблема.

Если ваша функция getSomeText может быть синхронной или асинхронной, в зависимости от контекста, то она не должна влиять на порядок выполнения. Вы используете promises, чтобы обеспечить его неизменность. Наличие переменного порядка выполнения, скорее всего, станет ошибкой в ​​вашем приложении.

Использование

getSomeText('first') // may be synchronous using cast or asynchronous with ajax
.then(print)
.then(function(){ print('second') });

В обоих случаях (синхронно с cast или асинхронным разрешением) у вас будет правильный порядок выполнения.

Обратите внимание, что наличие функции иногда синхронно, а иногда и не является странным или маловероятным случаем (подумайте об обработке кеша или объединении). Вы просто должны считать это асинхронным, и все будет всегда хорошо.

Но запрашивать у пользователя API точный с логическим аргументом, если он хочет, чтобы операция была асинхронной, кажется, не имеет никакого смысла, если вы не покидаете область JavaScript (т.е. если вы не используете некоторый собственный код).

Ответ 4

Здесь есть несколько хороших ответов, но очень кратко изложить суть дела:

Наличие обещания (или другого асинхронного API), которое иногда является асинхронным, а иногда и синхронным, является плохим.

Вы можете подумать, что это нормально, потому что первоначальный вызов вашего API принимает логическое значение для отключения между синхронизацией/асинхронизмом. Но что, если это похоронили в некотором коде обертки, а человек, использующий этот код, не знает об этих махинациях? Они только что закончили какое-то непредсказуемое поведение, не по своей вине.

Нижняя строка: не пытайтесь это сделать. Если вы хотите синхронного поведения, не возвращайте обещание.

С этим я оставлю вас с этой цитатой из Вы не знаете JS:

Еще одна проблема доверия называется "слишком рано". В специфических для приложения терминах это может фактически быть вызвано до того, как будет завершена какая-то критическая задача. Но в целом проблема очевидна в утилитах, которые могут либо вызывать обратный вызов, который вы предоставляете сейчас (синхронно), либо позже (асинхронно).

Этот недетерминизм вокруг синхронного или асинхронного поведения почти всегда приводит к очень сложному отслеживанию ошибок. В некоторых кругах вымышленный монстр, вызывающий безумие по имени Zalgo, используется для описания кошмаров sync/async. "Не отпускай Зальго!" это общий крик, и это приводит к очень здравым советам: всегда вызывать обратные вызовы асинхронно, даже если это "сразу" на следующем повороте цикла событий, так что все обратные вызовы предсказуемо асинхронны.

Примечание. Для получения дополнительной информации о Zalgo см. Oren Golan "Не выпускайте Zalgo!" (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) и Исаак З. Шлютер "Проектирование API для асинхронности" (<а2 > ).

Рассмотрим:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;`

Будет ли этот код печатать 0 (вызов обратного вызова синхронизации) или 1 (вызов вызова асинхронного вызова)? Зависит... от условий.

Вы можете видеть, насколько быстро непредсказуемость Zalgo может угрожать любой программе JS. Таким образом, глупо звучащий "никогда не выпускает Zalgo" на самом деле невероятно распространенный и солидный совет. Всегда будьте асинхронными.

Ответ 5

Как насчет этого случая, также связанный с CrmFetchKit, который в последней версии использует Bluebird. Я обновил версию 1.9, основанную на jQuery. Тем не менее старый код приложения, который использует CrmFetchKit, имеет методы, прототипы которых я не могу или не буду изменять.

Существующий код приложения

CrmFetchKit.FetchWithPaginationSortingFiltering(query.join('')).then(
    function (results, totalRecordCount) {
        queryResult = results;

        opportunities.TotalRecords = totalRecordCount;

        done();
    },
    function err(e) {
        done.fail(e);
    }
);

Старая реализация CrmFetchKit (пользовательская версия fetch())

function fetchWithPaginationSortingFiltering(fetchxml) {

    var performanceIndicator_StartTime = new Date();

    var dfd = $.Deferred();

    fetchMore(fetchxml, true)
        .then(function (result) {
            LogTimeIfNeeded(performanceIndicator_StartTime, fetchxml);
            dfd.resolve(result.entities, result.totalRecordCount);
        })
        .fail(dfd.reject);

    return dfd.promise();
}

Новая реализация CrmFetchKit

function fetch(fetchxml) {
    return fetchMore(fetchxml).then(function (result) {
        return result.entities;
    });
}

Моя проблема в том, что в старой версии был dfd.resolve(...), где я смог передать любое количество параметров, которые мне нужны.

Новая реализация просто возвращается, родительский, кажется, вызывает обратный вызов, я не могу назвать его напрямую.

Я пошел и сделал пользовательскую версию fetch() в новой реализации

function fetchWithPaginationSortingFiltering(fetchxml) {
    var thePromise = fetchMore(fetchxml).then(function (result) {
        thePromise._fulfillmentHandler0(result.entities, result.totalRecordCount);
        return thePromise.cancel();
        //thePromise.throw();
    });

    return thePromise;
}

Но проблема в том, что обратный вызов вызывается два раза, один раз, когда я делаю это явно и второй с помощью фреймворка, но он передает ему только один параметр. Чтобы обмануть его и "сказать", чтобы не называть ничего, потому что я делаю это явно, я пытаюсь вызвать .cancel(), но он игнорируется. Я понял, почему, но все же, как вы делаете "dfd.resolve(result.entities, result.totalRecordCount)"; в новой версии, не имея необходимости менять прототипы в приложении, использующем эту библиотеку?

Ответ 6

На самом деле вы можете сделать это, да.

Измените файл bluebird.js (для npm: node_modules/bluebird/js/release/bluebird.js) со следующим изменением:

[...]

    target._attachExtraTrace(value);
    handler = didReject;
}

- async.invoke(settler, target, {
+ settler.call(target, {
    handler: domain === null ? handler
        : (typeof handler === "function" &&

[...]

Для получения дополнительной информации см. здесь: https://github.com/stacktracejs/stacktrace.js/issues/188