Дополнить обещание ES6 методом отмены

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

sscce, который иллюстрирует то, что я пытаюсь сделать, выглядит следующим образом:

function TimerPromise(timeInterval) {
    var timer;

    var p = new Promise(
        function(resolve,reject) {
            timer = setTimeout(
                function() {
                    resolve(true);
                },
                timeInterval
            );
        }
    );

    p.cancel = function() {
        clearTimeout(timer);
    };

    console.log("p.cancel is ",p.cancel);

    return p;
}

var t = TimerPromise(2000).then(function(res) { console.log("Result is ",res); });

t.cancel();

В примере TimerPromise просто устанавливает таймер для имитации длительной асинхронной активности.

Вот что я получаю при запуске:

$ node test.js
p.cancel is  function () {
                timer.clearTimeout();
        }
/home/harmic/js/src/test.js:28
t.cancel();
  ^

TypeError: t.cancel is not a function
    at Object.<anonymous> (/home/harmic/js/src/test.js:28:3)
    at Module._compile (module.js:413:34)
    at Object.Module._extensions..js (module.js:422:10)
    at Module.load (module.js:357:32)
    at Function.Module._load (module.js:314:12)
    at Function.Module.runMain (module.js:447:10)
    at startup (node.js:141:18)
    at node.js:933:3

По какой-то причине метод отмены, который я добавил в обещание, исчез после выхода из функции!

Есть ли какая-то причина, по которой я не могу добавлять свойства или методы в обещание ES6? Или это какая-то особенность реализации Promises в V8?

Для бонусных баллов - я хотел бы иметь возможность отклонить обещание, если вызывается метод отмены, что-то вроде этого:

p.cancel = function() {
    timer.clearTimeout();
    reject(new Error("Request Cancelled"));
};

Однако я не могу получить доступ к функции отклонения вне исполнителя обещания, а внутри исполнителя обещания я не могу получить доступ к самому обещанию (или могу?), поэтому я не могу увеличить обещание внутри него.

Есть ли разумный шаблон для этого?

Примечание. Мне известно, что Bluebird предоставляет отменяемые обещания в качестве расширения. Я надеялся реализовать это с помощью собственных промисов ES6.

Я использую node.js 0.12, хотя я бы хотел, чтобы это работало и в текущих браузерах.


person harmic    schedule 04.03.2016    source источник
comment
Вы можете увеличить промис, как обычно, вы увидите, что вы можете вызвать TimerPromise(2000).cancel() напрямую, но затем вернете новый собственный промис.   -  person Paul    schedule 04.03.2016
comment
Ваш полифилл не очень хорош - вам нужна семантика отмены третьего состояния, поскольку семантика неудачной исключительной ситуации не верна. Ни один из ответов не дает того, что я бы рекомендовал использовать - вместо этого вы хотите создать подкласс обычных обещаний и предоставить им эту функциональность, переопределив then и добавив finally. Я опубликую ответ когда-нибудь.   -  person Benjamin Gruenbaum    schedule 04.03.2016


Ответы (3)


Проблема в том, что вызов then вернет новый объект обещания, поэтому вы теряете ссылку на свое обещание с помощью метода отмены.

function TimerPromise(timeInterval) {
  var timer;

  var p = new Promise(
    function(resolve, reject) {
      timer = setTimeout(
        function() {
          resolve(true);
        },
        timeInterval
      );
    }
  );

  p.cancel = function() {
    clearTimeout(timer);
  };

  console.log("p.cancel is ", p.cancel);

  return p;
}

var t = TimerPromise(2000);
t.then(function(res) {
  console.log("Result is ", res);
});

t.cancel();

person Arun P Johny    schedule 04.03.2016

В дополнение к ответу Аруна вы можете довольно легко решить проблему reject, не находящейся в области действия, сделав ссылку на нее, которая является моей областью:

var timer;
var reject;

var p = new Promise(
  function(resolve, _reject) {
    reject = _reject;
    timer = setTimeout(
      function() {
         resolve(true);
      },
      timeInterval
    );
  }
);

p.cancel = function() {
  clearTimeout(timer);
  reject(new Error("Request Cancelled")); 
};
person Paul    schedule 04.03.2016
comment
Я понимаю проблему, которую вы пытаетесь решить, но то, как вы ее решаете, выглядит уродливо. Не лучше ли поместить реализацию отмены внутрь функции обещания (где у нее есть доступ к аргументу reject)? - person jfriend00; 05.03.2016

Я не понимаю, почему вы должны думать об «отмене» обещания. Вместо этого вы можете подумать о предоставлении интерфейса для отклонения этого. Вам не нужно беспокоиться об отмене таймера, потому что, если обещание уже было отклонено вручную, то даже когда таймер сработает и будет вызван resolve, это не будет иметь никакого эффекта.

function TimerPromise(timeInterval) {
  var _reject;

  var p = new Promise(function(resolve, reject) {
     // Remember reject callback in outside scope.
      _reject = reject;

      setTimeout(() => resolve(true), timeInterval);
  });

  // Attach reject callback to promise to let it be invocable from outside.
  p.reject= _reject();

  return p;
}

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

var t1 = TimerPromise(2000);
var t2 = t1.then(function(res) { console.log("Result is ", res); });

t1.reject();

Термин «отменить» по отношению к обещаниям имеет другое, более сложное значение, в которое я не буду вдаваться. Некоторые библиотеки предоставляют эту функцию; нативных обещаний нет, хотя обсуждается их добавление в будущую версию ES.

person Community    schedule 04.03.2016
comment
Это единственный разумный ответ здесь с точки зрения того, чтобы не создавать удивительный код. Он делает то, что рекламируется, и отвергает обещание. +1 - person Benjamin Gruenbaum; 04.03.2016
comment
Тем не менее, есть некоторая ценность в выполнении clearTimeout() для повышения эффективности системных ресурсов, когда таймер больше не нужен. - person jfriend00; 05.03.2016
comment
Извините за поздний отзыв. Использование таймера в SSCCE предназначено для демонстрационных целей, фактическая асинхронная активность, которую необходимо отменить, использует реальные ресурсы и действительно должна быть отменена, а не просто игнорируется результат. Например, запрос запрошен на удаленном сервере. - person harmic; 15.04.2016