Цикл while с промисами A+ в JavaScript/node.js

Мне нужно продолжать вызывать удаленный API, пока я не получу нужный мне ответ, и я хотел бы использовать официальные обещания A+ в node.js. Синхронизировать псевдокод:

params = { remote api call params }
while (true) {
    result = callRemoteApi(params)
    if isGood(result) {
        onSuccess(result)
        break
    }
    params = modify(params)
}

Я использую библиотеку request-promise для запросов, поэтому результат может быть примерно таким это:

new Promise(function (resolve, reject) {
    var task = request({params})
        .then(function (result) {
            if (isGood(result)) {
                resolve(result);
            } else {
                task = request({new params}).then(this_function);
            }
        });

P.S. Это очень похоже на https://stackoverflow.com/a/17238793/177275, но я хотел бы не- реализация на основе q.


person Yuri Astrakhan    schedule 18.02.2015    source источник


Ответы (2)


Что-то вроде этого должно работать хорошо:

var remoteApiCall = function(num){
    // fake function to resolve promise if random number in range
    return new Promise(function(resolve, reject){
        return ((Math.random()*10) < num)
            ? resolve(true)
            : reject(false);
    })
}

function getUntil(num){
    return remoteApiCall(num).then(function(result){
        if (result) {
            return result  
        } else {
            // call again until you get valid response
            return getUntil(num)
        }
    })
}

getUntil(num).then(...)
person aarosil    schedule 18.02.2015
comment
Хорошее решение и в целом - спасибо за помощь с тегом обещания. Обратите внимание, что преимущество этого решения в том, что оно не зависит от платформы. - person Benjamin Gruenbaum; 18.02.2015
comment
Вы отказываетесь от undefined, в то время как вызывающий код просто ожидает разрешения с ложным значением - person Esailija; 18.02.2015
comment
@BenjaminGruenbaum спасибо и с удовольствием ... просто пытаюсь отплатить за помощь, которую я получил от всех вас - person aarosil; 19.02.2015
comment
@Esailija СПАСИБО .. Я почистил :) - person aarosil; 19.02.2015

Следующее решение решает несколько конкретных проблем:

  • как разорвать (иначе бесконечный) цикл
  • как получить доступ к результату ранее неудачной попытки
  • как включить params = modify(params)

Проблема может быть разбита на две части:

  • повторитель/промис-бегун (который сам возвращает промис, поэтому мы можем подключить к нему onSuccess)
  • поставщик обещаний - функция, которая создает обещания для повторителя

Повторитель будет выглядеть так:

function repeatUntilSuccess(promiseProvider) {
    return new Promise(function (resolve, reject) {
        var counter = 0;
        function run(failedResult) {
            var p = promiseProvider(failedResult, counter++);
            if (p instanceof Promise) {
                p.then(resolve).catch(run);
            } else {
                reject(p);
            }
        }
        run();
    });
}

В этой строке происходит «петля»: p.then(resolve).catch(run);. Повторитель продолжает вызывать поставщика обещаний до тех пор, пока возвращаемое им обещание не будет разрешено (в этом случае повторитель разрешается) или до тех пор, пока он больше не предоставляет обещание (в этом случае повторитель отклоняет).

Поставщик обещаний может быть любым function(previousResult, counter), который возвращает обещание (или нет, если вы хотите остановить цикл).

var params = {/* ... */};
function nextRequest(previousResult, counter) {
    if (counter >= 10) return "too many attempts";
    if (previousResult) params = modify(params);
    return apiRequest(params);
}

(Эта установка предполагает, что apiRequest() возвращает обещание)

Теперь вы можете сделать это:

repeatUntilSuccess(nextRequest).then(onSuccess).catch(onError);

Поскольку ваш вопрос включает в себя побочную задачу по заключению HTTP-запроса в обещание:

function apiRequest(params) {
    return new Promise(function (resolve, reject) {
        return request(params).then(function (result) {
            if (isGood(result)) {
                resolve(result);
            } else {
                reject(result);
            }
       });
    });
}

Откройте консоль браузера и запустите следующий фрагмент кода, чтобы увидеть его в действии.

function repeatUntilSuccess(promiseProvider) {
    return new Promise(function (resolve, reject) {
        var counter = 0;
        function run(failedResult) {
            var p = promiseProvider(failedResult, counter++);
            if (p instanceof Promise) {
                p.then(resolve).catch(run);
            } else {
                reject(p);
            }
        }
        run();
    });
}

// mockup promise that resoves or rejects randomly after a timeout
function randomPromise(num){
    return new Promise(function(resolve, reject){
        setTimeout(function () {
            var randomNum = Math.floor(Math.random() * num * 10);
            if (randomNum < num) {
                resolve(randomNum);
            } else {
                reject(randomNum);
            }
        }, 300);
    });
}

// promise provider
function nextPromise(prev, i) {
    if (prev) console.info("failed attempt #" + i + ": " + prev);
    if (i >= 5) return "too many attempts:" + i;
    return randomPromise(100);
}

// run it!
repeatUntilSuccess(nextPromise).then(function (result) {
    console.log("success", result);
}).catch(function (result) {
    console.log("failed", result);
});

person Tomalak    schedule 18.02.2015
comment
stackoverflow.com/questions/23803743/ - person Benjamin Gruenbaum; 19.02.2015
comment
@BenjaminGruenbaum Я не согласен. Мой код выглядит так, но это не так. (Хорошо, apiReques() можно сделать немного проще, но все остальное?) - person Tomalak; 19.02.2015
comment
1) К сожалению, ваша последняя версия в Gist больше не эквивалентна моей первоначальной идее. 2) Я не понимаю, чем Promise.resolve() как способ создания нового промиса лучше, чем new Promise(). Это меньше строк, но лучше? (Хотя я понимаю, что Promise.resolve(p) лучше.) 3) Я не согласен с тем, что отклоненные обещания подобны исключениям. Если обещание инкапсулирует процесс, который может потерпеть неудачу естественным образом (например, установка значения и возврат не разрешенного), то я ожидаю, что это обещание будет отклонено, а не разрешено с ложным значением. - person Tomalak; 19.02.2015
comment
Может быть, я ошибаюсь с моим последним пунктом. Предположим, что я общаюсь с HTTP API, чтобы установить значение foo равным 5. Этот запрос может завершиться ошибкой на нескольких уровнях. Он может выйти из строя физически (тайм-аут), он может выйти из строя на сервере (404 или 500), он может выйти из строя в приложении (отказано в доступе). Я ожидал отклоненного обещания в каждом из этих случаев, потому что обещание состояло в том, чтобы установить значение foo равным пяти, и это не сработало. Разрешение обещания со значением false для случая отказа в доступе кажется неправильным. - person Tomalak; 19.02.2015
comment
Последняя версия не эквивалентна, но только что предыдущая. Promise.resolve() просто дает вам пустое обещание - вам не нужно создавать свой собственный источник завершения, и ваш код не связан со всеми соединениями отклонений и ошибок, поэтому намного проще не ошибиться - ваша apiRequest функция может быть просто function apiRequest(){ request(params).then(function(res){ if(!isGood(res)) throw res; return res; });} . В API-интерфейсах HTTP я считаю, что отказ из-за тайм-аута или сбоя приложения — это хорошо, но я бы сам не отклонил ошибку 404 на уровне HTTP-запроса. - person Benjamin Gruenbaum; 19.02.2015
comment
Что ж, при правильном абстрагировании setFoo(5).then(joy).catch(noJoy) даже не раскрывает тот факт, что задействован HTTP. Когда сервер отвечает нет, это не ошибка HTTP, но это все равно означает для меня noJoy. Верно? - person Tomalak; 19.02.2015
comment
Вы говорите здесь о сбое RPC или сбое протокола? Когда вы делаете HTTP-запрос, оболочка обещания для этого должна отклонять, если сам запрос не удался, а не в том случае, если запрос был успешным и сервер вернул ответ, которого мы не ожидали. Оборачивая этот API в RPC API, вы можете проверить, не сделал ли сервер то, что мы ожидали, и throw что дало бы вам такое поведение. В любом случае, если вы хотите поговорить об этом подробнее, я часто бываю в JS-комнате в чате. Тут как-то стало оффтопом. - person Benjamin Gruenbaum; 19.02.2015
comment
Давайте продолжим обсуждение в чате. - person Tomalak; 19.02.2015