В JavaScript, как обернуть обещание в тайм-аут?

Это общий шаблон для реализации тайм-аута некоторой асинхронной функции с использованием deffered/prom:

// Create a Deferred and return its Promise
function timeout(funct, args, time) {
    var dfd = new jQuery.Deferred();

    // execute asynchronous code
    funct.apply(null, args);

    // When the asynchronous code is completed, resolve the Deferred:
    dfd.resolve('success');

    setTimeout(function() {
        dfd.reject('sorry');
    }, time);
    return dfd.promise();
}

Теперь мы можем выполнить некоторую асинхронную функцию с именем myFunc и обработать таймаут:

// Attach a done and fail handler for the asyncEvent
$.when( timeout(myFunc, [some_args], 1000) ).then(
    function(status) {
        alert( status + ', things are going well' );
    },
    function(status) {
        alert( status + ', you fail this time' );
    }
);

Хорошо, сделайте поворот в этой истории! Представьте себе, что сам myFunc возвращает обещание (ПРИМЕЧАНИЕ: обещание НЕ отложено, и я не могу его изменить):

function myFunc(){
    var dfd = new jQuery.Deffered();
    superImportantLibrary.doSomething(function(data)){
       if(data.length < 5){
            dfd.reject('too few data');
       }
       else{
           dfd.resolve('success!');
       }
    }, {'error_callback': function(){
        dfd.reject("there was something wrong but it wasn't timeout");}
    }});
    return dfd.promise();
}

Теперь, если я завершу myFunc в timeout, я потеряю способность обрабатывать ошибки, отличные от тайм-аута. Если myFunc испускает события прогресса, я тоже потеряю это.

Итак, возникает вопрос: как изменить функцию timeout, чтобы она могла принимать функции, возвращающие promises, не теряя своих ошибок/информацию о ходе?

Ответы

Ответ 1

function timeout(funct, args, time) {
    var deferred = new jQuery.Deferred(),
        promise = funct.apply(null, args);

    if (promise) {
        $.when(promise)
            .done(deferred.resolve)
            .fail(deferred.reject)
            .progress(deferred.notify);
    }

    setTimeout(function() {
        deferred.reject();
    }, time);

    return deferred.promise();
}

Ответ 2

Вы всегда должны пробовать на самом низком уровне. Давайте начнем с основ.

Я буду использовать jQuery promises здесь, но это действительно должно быть сделано с более сильной библиотекой, такой как Bluebird. Пусть начнется просто, создав наш delay как:

function delay(ms){
    var d = $.Deferred();
    setTimeout(function(){ d.resolve(); }, ms);
    return d.promise();
}

Задержка записи не делает ничего удивительного, вся наша функция задержки вызывает задержку ms миллисекунд.

Теперь для вашей библиотеки мы хотим создать версию doSomething, которая работает с promises:

 superImportantLibrary.doSomethingAsync = function(){
     var d = $.Deferred();
     superImportantLibrary.doSomething(function(data){ d.resolve(data); });
     return d.promise();
 };

Обратите внимание, что обе наши функции задержки и doSomethingAsync выполняют как одну вещь. Теперь начинается веселье.

function timeout(promise,ms){
    var timeout = delay(ms); // your timeout
    var d = $.Deferred();
    timeout.then(function(){ d.reject(new Error("Timed Out")); });
    promise.then(function(data){ d.resolve(data); });
    return d.promise();
}

timeout(superImportantLibrary.doSomethingAsync(),1000).then(function(data){
     // handle success of call
}, function(err){
     // handle timeout or API failure.
});

Теперь в Bluebird весь этот код был бы:

superImportantLibrary.doSomethingAsync().timeout(1000).then(function(){
    // complete and did not time out.
});

Ответ 3

Я понимаю, что это 2 года, но в случае, если кто-то ищет ответ...

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

function delay(ms){
    var d = $.Deferred();
    setTimeout(function(){ d.resolve(); }, ms);
    return d.promise();
}

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

function timeout(funct, args, time) {
    return delay(time).then(function(){
        // Execute asynchronous code and return its promise
        // instead of the delay promise. Using "when" should
        // ensure it will work for synchronous functions as well.
        return $.when(funct.apply(null, args));
    });
}

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

function timeout(funct, args, time) {
    var d = $.Deferred();

    // Call the potentially async funct and hold onto its promise.
    var functPromise = $.when(funct.apply(null, args));

    // pass the result of the funct to the master defer
    functPromise.always(function(){
        d.resolve(functPromise)
    });

    // reject the master defer if the timeout completes before
    // the functPromise resolves it one way or another
    delay(time).then(function(){
        d.reject('timeout');
    });

    // To make sure the functPromise gets used if it finishes
    // first, use "then" to return the original functPromise.
    return d.then(function(result){
        return result;
    });
}

Мы можем упростить это, зная, что в этом случае главный defer отклоняется только в том случае, если время ожидания происходит первым, и разрешается только в том случае, если functPromise разрешается первым. Из-за этого нам не нужно передавать functPromise в разрешение master defer, потому что это единственное, что может быть передано, и мы все еще в области видимости.

function timeout(funct, args, time) {
    var d = $.Deferred();

    // Call the potentially async funct and hold onto its promise.
    var functPromise = $.when(funct.apply(null, args))
        .always(d.resolve);

    // reject the master defer if the timeout completes before
    // the functPromise resolves it one way or another
    delay(time).then(function(){
        d.reject('timeout');
    });

    // To make sure the functPromise gets used if it finishes
    // first, use "then" to return the original functPromise.
    return d.then(function(){
        return functPromise;
    });
}