Как разрешить рекурсивные асинхронные промисы?

Я играю с обещаниями, и у меня проблемы с асинхронным рекурсивным обещанием.

Сценарий: спортсмен начинает бежать 100 м, мне нужно периодически проверять, закончил ли он, и как только он закончил, распечатать его время.

Изменить для уточнения :

В реальном мире спортсмен работает на сервере. startRunning включает вызов ajax на сервер. checkIsFinished также включает вызов ajax на сервер. Приведенный ниже код является попыткой имитировать это. Время и расстояние в коде жестко закодированы в попытке сделать все максимально простым. Извиняюсь за неясность.

Завершить редактирование

Я хотел бы иметь возможность написать следующее

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)

куда

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var checkIsFinished = function (athlete) {
  return new Promise(function (resolve, reject) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);

    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(checkIsFinished.bind(null, athlete), 1000);
    }    
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log(e); };

Я вижу, что обещание, созданное в первый раз checkIsFinished, никогда не разрешается. Как я могу гарантировать, что это обещание разрешено, чтобы вызвать printTime?

Вместо

resolve(athlete);

я мог бы сделать

Promise.resolve(athlete).then(printTime);

Но я хотел бы избежать этого, если это возможно, я действительно хотел бы иметь возможность писать

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)

person user5325596    schedule 11.09.2015    source источник
comment
Этот код действительно сложен для понимания, но похоже, что он действительно неправильно использует промисы. Если я не совсем понял это неправильно, я думаю, вам следует уделить несколько минут, чтобы снова подумать об этом дизайне, возможно, сначала прочитать о обещаниях.   -  person Amit    schedule 11.09.2015
comment
Вы должны понимать, что каждое .then() выполняется только один раз, а обещания разрешаются или отклоняются один раз и навсегда. Итак, в первый раз, когда вы достигаете checkIsFinished, вы возвращаете обещание, которое оставляете в ожидании, а затем снова рекурсивно вызываете себя, используя setTimeout. Но это новое обещание никуда не денется, так как setTimeout просто возвращает дескриптор, а не обещание, и поэтому это новое обещание также остается в ожидании, а затем вы повторяете - вероятно, вызывая утечку памяти. Потратьте время, чтобы прочитать и понять тонкости цикла обработки событий Javascript. Это сэкономит вам много времени и нервов.   -  person caasjj    schedule 11.09.2015
comment
когда вы делаете setTimeout(checkIsFinished.bind(null, athlete), 1000);, вы создаете внутри новое обещание, так что старое обещание никогда не решается   -  person Grundy    schedule 12.09.2015
comment
@Amit, я отредактировал свой вопрос, чтобы прояснить ситуацию.   -  person user5325596    schedule 12.09.2015
comment
@caasjj, @Grundy, спасибо за комментарии. Я понимаю, что setTimeout(checkIsFinished.bind(null, athlete), 1000) создает новое обещание, а исходное обещание теряется. Мой вопрос в основном есть способ обойти это? Я отредактировал вопрос, пытаясь прояснить ситуацию.   -  person user5325596    schedule 12.09.2015


Ответы (3)


Ошибка в том, что вы передаете функцию, которая возвращает обещание setTimeout. Это обещание теряется в эфире. Временное исправление может заключаться в рекурсии функции-исполнителя:

var checkIsFinished = function (athlete) {
  return new Promise(function executor(resolve) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);
    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(executor.bind(null, resolve), 1000);
    }    
  });
};

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

Лучшие практики, которым я следую, чтобы избежать таких ошибок:

  1. Работайте только с асинхронными функциями, которые возвращают промисы.
  2. Если обещание не возвращается, оберните его конструктором обещания.
  3. Оберните его как можно более узко (с минимальным количеством кода).
  4. Не используйте конструктор промисов ни для чего другого.

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

Применение этого к вашему примеру привело меня сюда (для краткости я использую функции стрелок es6. Они работают в Firefox и Chrome 45):

var console = { log: msg => div.innerHTML += msg + "<br>",
                error: e => console.log(e +", "+ e.lineNumber) };

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));

var startRunning = () => {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0,
    intervalID: setInterval(() => {
      athlete.distanceTravelled += 25;
      athlete.timeTaken += 2.5;
      console.log("updated athlete ");
    }, 2500)
  };
  return wait(2000).then(() => athlete);
};

var checkIsFinished = athlete => {
  if (athlete.distanceTravelled < 100) {
    console.log("not finished yet, check again in a bit");
    return wait(1000).then(() => checkIsFinished(athlete));
  }
  clearInterval(athlete.intervalID);
  console.log("finished");
  return athlete;
};

startRunning()
  .then(checkIsFinished)
  .then(athlete => console.log('printing time: ' + athlete.timeTaken))
  .catch(console.error);
<div id="div"></div>

Обратите внимание, что checkIsFinished возвращает либо спортсмена, либо обещание. Здесь это нормально, потому что .then функции автоматически продвигают возвращаемые значения из функций, которые вы передаете в промисы. Если вы будете вызывать checkIsFinished в других контекстах, вы можете сделать повышение самостоятельно, используя return Promise.resolve(athlete); вместо return athlete;.

Редактировать в ответ на комментарии Амита:

Для нерекурсивного ответа замените всю функцию checkIsFinished этим помощником:

var waitUntil = (func, ms) => new Promise((resolve, reject) => {
  var interval = setInterval(() => {
    try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
  }, ms);
});

а затем сделайте это:

var athlete;
startRunning()
  .then(result => (athlete = result))
  .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
  .then(() => {
    console.log('finished. printing time: ' + athlete.timeTaken);
    clearInterval(athlete.intervalID);
  })
  .catch(console.error);

var console = { log: msg => div.innerHTML += msg + "<br>",
                error: e => console.log(e +", "+ e.lineNumber) };

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));

var waitUntil = (func, ms) => new Promise((resolve, reject) => {
  var interval = setInterval(() => {
    try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
  }, ms);
});

var startRunning = () => {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0,
    intervalID: setInterval(() => {
      athlete.distanceTravelled += 25;
      athlete.timeTaken += 2.5;
      console.log("updated athlete ");
    }, 2500)
  };
  return wait(2000).then(() => athlete);
};

var athlete;
startRunning()
  .then(result => (athlete = result))
  .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
  .then(() => {
    console.log('finished. printing time: ' + athlete.timeTaken);
    clearInterval(athlete.intervalID);
  })
  .catch(console.error);
<div id="div"></div>

person jib    schedule 12.09.2015
comment
Хотя ваш список лучших практик выглядит компетентным, он приводит вас к сценарию, в котором вы рекурсивно создаете новые промисы и вместо цепочки создаете все более и более глубокий стек обещаний. Каждый wait(1000).then(...) размещает новый Promise - аналогичный wait(x).then(()=>wait(x).then(()=>wait(x).then(...))). Это окажет негативное влияние на память и производительность, если/когда будет получен конечный результат. Хуже всего то, что все эти детали скрыты за кричащим синтаксисом и абстракцией. - person Amit; 13.09.2015
comment
@Амит, ты совершенно не прав. Во-первых, здесь нет рекурсии стека, так как вещи не добавляются в одном прогоне. Нет построения цепочки заранее. Все, что происходит, это то, что каждую секунду создается новое обещание, точно так же, как в вашем примере, и в тот же момент все ссылки на предыдущее обещание исчезают, что делает его сборщиком мусора. Это может продолжаться вечно без каких-либо проблем с памятью. Не существует цепочки, построенной так, как вы ее себе представляете. Мое предпочтительное решение здесь будет использовать setInterval вместо этого, но ваш вопрос задал рекурсивный, так что вот и все. - person jib; 13.09.2015
comment
Не уверен, на какой мой вопрос вы ссылаетесь, но обещания не могут быть собраны из-за того, что я объяснил в своем предыдущем комментарии. Каждое обещание разрешается, а затем вызывается .then() с обратным вызовом, который возвращает новое обещание. Затем этот вызов в конечном итоге будет разрешен к тому, на что разрешается внутреннее обещание, но пока это не произойдет, он там и ждет. И так продолжается и продолжается рекурсивно. Тот факт, что ничего не происходит раньше времени, не имеет значения, поскольку, если конечное событие никогда не наступит, вы будете создавать обещания до тех пор, пока не закончится память. - person Amit; 13.09.2015
comment
@Amit Я совершенно убежден, что реализации обещаний могут оптимизировать это, как указано в этом ответе на связанный вопрос, но здесь нет места, чтобы поделиться своим анализом. Однако я признаю, что не знаю, делают ли это какие-либо реализации, но я попытаюсь выяснить это. - Несмотря на то, что ваш вопрос называется «Как разрешить рекурсивные асинхронные промисы?», я отредактировал свой ответ, чтобы предоставить нерекурсивный ответ, который, я надеюсь, удовлетворит ваши опасения по поводу использования памяти. - person jib; 13.09.2015
comment
Еще раз: я не задавал вопросов, но указал на недостаток в вашем дизайне. (Отличная) ссылка в вашем предыдущем комментарии доказывает мою точку зрения, я не понимаю, как вы это запутали. Отредактированное вами решение выглядит лучше, склоняется к вашему собственному ответу и может быть хорошим, но, по крайней мере, для меня оно довольно запутанно/нечитаемо (функция waitUntil), хотя оно делает очень краткий код в вызове .then(). Если это действительно работает, мне это нравится - person Amit; 13.09.2015
comment
Мои извинения @Amit, я перепутал вас с ОП, и вы можете быть правы насчет существующих реализаций. Спасибо за ваш отзыв, я отредактировал функцию waitUntil, чтобы сделать ее более понятной. Только что заметил твой ответ. - person jib; 13.09.2015
comment
@Amit: Рекурсивное решение совершенно нормальное - читаемое и понятное, это предпочтительный способ. Если этот код занимает память и создает проблемы с производительностью, это ошибка реализации промиса. - person Bergi; 13.09.2015
comment
@Bergi Берги Я не знаю о предпочтительном, но это то, о чем спрашивал ОП. Я согласен с Амитом в том, что опросов следует избегать, и случаи, когда они мне нужны, редки. Хотя я думаю, что это все еще действительная иллюстрация для вопроса. - person jib; 13.09.2015
comment
@Bergi ваш собственный ответ (связанный стрелой выше) доказывает, что у меня проблемы с производительностью. Я не понимаю, как вы отвергаете это утверждение. Что касается нечитаемой части, то этот комментарий был не о рекурсивном решении, а о предыдущем (теперь улучшенном) нерекурсивном решении. - person Amit; 13.09.2015
comment
@Amit: Да, и, как я уже писал там, проблемы с производительностью - это ошибка библиотеки, и их можно исправить, если они действительно появятся. Рекурсия по-прежнему лучший дизайн imo. - person Bergi; 13.09.2015
comment
@Берги. Спасибо за ваши комментарии. - person user5325596; 14.09.2015
comment
Из трех решений в этом ответе первое и третье небезопасны с точки зрения обработки исключений, а второе создает проблемы с производительностью при использовании общих реализаций, включая ES2015 (как я отмечал выше). Небезопасная часть (1 и 3) возникает из-за того, что асинхронные исключения не перехватываются объектом Promise и не распространяются на возможный обработчик .catch(). Так будет вести себя код, который выполняется внутри .setTimeout()/.setInterval(). Чтобы увидеть это, добавьте new Error() перед resolve(athlete) и внутри func() (попробуйте athlete.a()) в решениях 1 и 3 соответственно. - person Amit; 16.09.2015
comment
@Amit Хорошая мысль о setInterval. Именно поэтому я выступаю за наименьшие возможные обертки, такие как wait вокруг опасных setTimeout. setInterval сложнее. Я обновил функцию waitUntil в нерекурсивном (третьем) решении с помощью try/catch, как вы показываете, для правильного распространения ошибок. ES2015 не является реализацией. - person jib; 17.09.2015
comment
Так-то лучше. Однако я бы внес еще одно изменение... Я бы реструктурировал и переименовал waitUntil в pollResolveWhen и добавил третий параметр для разрешения. Тогда вызов будет pollResolveWhen(() => athlete.distanceTravelled >= 100, 1000, athlete), и вы сможете исключить глобальное и предоставить решение, соответствующее требованиям OP. - person Amit; 17.09.2015
comment
@Amit - в своем первом коммите вы говорите, что каждый wait(1000).then(...) размещает новое обещание - сродни wait(x).then(()=>wait(x).then(()=>wait(x).then(...))). Не правильнее ли сказать, что это сродни wait(x).then(()=>wait(x)).then(()=>wait(x)).then(...)? - person user5325596; 18.09.2015
comment
@ user5325596 - Определенно нет. Вот почему я разместил этот комментарий, потому что он делает именно то, на что я указал. Просто взглянув на код, вы можете увидеть, что за каждым wait() непосредственно следует .then(), который не может перейти во внешнее обещание. Так же можете попробовать сами, прикрепить еще then() после первого, и console.log что-то там. Вы увидите, что все журналы происходят одновременно, когда вся цепочка разрешена. Теперь вы можете безопасно использовать последнее решение в этом ответе - jib исправил его - но это не тот шаблон, о котором вы просили (мой :-). - person Amit; 18.09.2015
comment
@ user5325596 Обе ваши аналогии с ожиданием являются ошибочными сравнениями, поскольку они строят цепочки обещаний, то есть цепочки выполнения, тогда как рекурсивное .then использование просто создает цепочки разрешения, которые отличаются. @Bergi и я утверждаем, что цепочки разрешения можно и нужно оптимизировать, но Амит утверждает, что это не так. Это не подтверждается добавлением .then(() => console.log()) промежуточных шагов, потому что это меняет вещи: они добавляют код, который должен выполняться сразу при разрешении, что препятствует оптимизации с помощью реализаций. Парадокс наблюдателя. - person jib; 18.09.2015

Использование setTimeout / setInterval — это один из сценариев, который плохо сочетается с промисами и заставляет вас использовать антишаблон хмурого промиса.

Сказав это, если вы реконструируете свою функцию, сделаете ее функцией типа «ожидание завершения» (и назовите ее соответствующим образом), вы сможете решить свою проблему. Функция waitForFinish вызывается только один раз и возвращает один промис (хотя и новый, поверх исходного промиса, созданного в startRunning). Обработка повторения через setTimeout выполняется во внутренней функции опроса, где используется надлежащий try/catch для обеспечения распространения исключений на промис.

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var waitForFinish = function (athlete) {
  return new Promise(function(resolve, reject) {
    (function pollFinished() {
      try{
        if (athlete.distanceTravelled >= 100) {
          clearInterval(intervalID);
          console.log("finished");
          resolve(athlete);
        } else {
          if(Date.now()%1000 < 250) { // This is here to show errors are cought
            throw new Error('some error');
          }
          console.log("not finished yet, check again in a bit");
          setTimeout(pollFinished, 1000);
        }
      }
      catch(e) { // When an error is cought, the promise is properly rejected
        // (Athlete will keep running though)
        reject(e);
      }
    })();
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log('Handling error:', e); };

startRunning()
  .then(waitForFinish)
  .then(printTime)
  .catch(handleError);

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

person Amit    schedule 12.09.2015
comment
Я согласен, что решение на основе сокетов было бы предпочтительнее. К сожалению, на данный момент это невозможно. - person user5325596; 14.09.2015
comment
ваше решение, похоже, не работает. Когда я запускаю фрагмент кода, я вижу несколько сообщений формы updated athlete Object { timeTaken: 2.5, distanceTravelled: 25 } в консоли, но нет сообщений not finished yet и сообщений finished. - person user5325596; 14.09.2015
comment
@ user5325596 - запустите его несколько раз. Я намеренно добавил случайный код генерации исключений, который показывает, как это решение обрабатывает исключения (что может произойти в вашем реальном коде). При возникновении такого исключения вы не увидите finished. Вы должны очистить эту часть кода для реального использования. (Соответствующая строка if(Date.now()%1000 < 250)) - person Amit; 14.09.2015
comment
@ user5325596 - Кстати, вы должны знать, что, по крайней мере, насколько я понимаю (я совершенно уверен в этом), во всех решениях в вашем текущем принятом ответе отсутствует обработка исключений (попробуйте добавить их внутрь) или возникают проблемы с памятью / производительностью. Вы должны учитывать последствия их использования. - person Amit; 14.09.2015
comment
@Amit. Обратите внимание, что исключения внутри функций .then и функций-исполнителей конструктора обещаний автоматически вызывают отклонение, поэтому ваша обработка исключений является избыточной. Принятый ответ имеет достаточную обработку ошибок, поскольку любые ошибки кодирования внутри синхронной части startRunning будут выведены на консоль JS, а последующая цепочка обещаний правильно завершается с помощью .catch, который сообщает об ошибках. Принятый ответ также имеет нерекурсивное решение без споров о производительности. - person jib; 16.09.2015
comment
@jib - вы не только ошибаетесь, обработка исключений критически важна. исполнитель Promise / функция .then() обрабатывает только SYNCHRONOUS исключения. Код внутри .setTimeout() / .setInterval, который вызывает броски, не будет перехвачен промисом и не будет передан конечному обработчику .catch(). Тот факт, что движок поймает и сообщит об этом в консоль, не имеет значения - поэтому в первую очередь есть вызов .catch(). Это именно проблема с вашим ответом (ами). - person Amit; 16.09.2015
comment
Спасибо за продолжение комментариев. Я был занят другой работой, но теперь у меня есть немного времени, чтобы поэкспериментировать с этим. - person user5325596; 16.09.2015

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

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

startRunning(100).then(printTime, handleError);

Вы можете реализовать это с помощью такого кода:

function startRunning(limit) {
    return new Promise(function (resolve, reject) {
        var timeStart = Date.now();
        var athlete = {
            timeTaken: 0,
            distanceTravelled: 0
        };
        function updateAthlete() {
            athlete.distanceTravelled += 25;
            console.log("updated athlete", athlete)
            if (athlete.distanceTravelled >= limit) {
                clearInterval(intervalID);
                athlete.timeTaken = Date.now() - timeStart;
                resolve(athlete);
            }
        }
        var intervalID = setInterval(updateAthlete, 2500);
    });
}

function printTime(athlete) {
    console.log('printing time', athlete.timeTaken);
}

function handleError(e) { 
    console.log(e); 
}

startRunning(100).then(printTime, handleError);

Рабочая демонстрация: http://jsfiddle.net/jfriend00/fbmbrc8s/


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


Вот некоторые из фундаментальных вещей, которые вы допустили неправильно при использовании промисов:

  1. Это одноразовые объекты. Они решаются или отклоняются только один раз.
  2. Структура startRunning().then(checkIsFinished) просто не имеет логического смысла. Чтобы первая часть этого работала, startRunning() должен вернуть обещание, и он должен разрешить или отклонить это обещание, когда произойдет что-то полезное. Вы просто разрешаете это через две секунды, что, похоже, не приносит ничего полезного.
  3. Слова вашего описания звучат так, как будто вы хотите, чтобы `checkIsFinished() продолжала работать, а не выполняла свое обещание, пока спортсмен не закончит. Это можно сделать, постоянно связывая промисы, но это кажется очень сложным способом делать что-то и, конечно, здесь не нужно. Кроме того, это совсем не то, что пытается сделать ваш код. Ваш код просто возвращает новое обещание, которое никогда не разрешается, если спортсмен уже не преодолел желаемое расстояние. Если нет, он возвращает обещание, которое никогда не разрешается и не отклоняется. Это фундаментальное нарушение концепций обещаний. Функция, которая возвращает обещание, отвечает за его окончательное разрешение или отклонение, если только вызывающий код не ожидает просто отказаться от обещания, и в этом случае это, вероятно, неправильный инструмент проектирования.

Вот еще один подход, который создает общедоступный объект Athlete(), который имеет некоторые методы и позволяет нескольким людям наблюдать за ходом выполнения:

var EventEmitter = require('events');

function Athlete() {
    // private instance variables
    var runInterval, startTime; 
    var watcher = new EventEmitter();

    // public instance variables
    this.timeTaken = 0;
    this.distanceTravelled = 0;
    this.startRunning = function() {
        startTime = Date.now();
        var self = this;
        if (runInterval) {clearInterval(runInterval);}
        runInterval = setInterval(function() {
            self.distanceTravelled += 25;
            self.timeTaken = Date.now() - startTime;
            console.log("distance = ", self.distanceTravelled);
            // notify watchers
            watcher.emit("distanceUpdate");
        },2500);
    }
    this.notify = function(limit) {
        var self = this;
        return new Promise(function(resolve, reject) {
            function update() {
                if (self.distanceTravelled >= limit) {
                    watcher.removeListener("distanceUpdate", update);
                    resolve(self);
                    // if no more watchers, then stop the running timer
                    if (watcher.listeners("distanceUpdate").length === 0) {
                        clearInterval(runInterval);
                    }
                }
            }
            watcher.on("distanceUpdate", update);
        });
    }
}

var a = new Athlete();
a.startRunning();
a.notify(100).then(function() {
    console.log("done");
});
person jfriend00    schedule 11.09.2015
comment
Спасибо за ответы. Я отредактировал свой вопрос, чтобы прояснить ситуацию. Написанный код — это просто попытка имитировать реальный сценарий, когда спортсмен находится на сервере. - person user5325596; 12.09.2015
comment
@user5325596 user5325596 - в следующий раз, пожалуйста, опишите свою РЕАЛЬНУЮ ситуацию, чтобы мы не тратили столько времени на работу над неправильной проблемой - довольно неприятно! Итак, хотите ли вы, чтобы checkIsFinished() возвращал обещание, которое разрешается, когда оно фактически завершено, и это будет определяться путем опроса вашего внутреннего сервера checkIsFinished с повторными вызовами ajax, чтобы увидеть, когда это на самом деле сделано? - person jfriend00; 12.09.2015
comment
@user5325596 user5325596 - извините, но вы слишком мало участвуете в этом вопросе (дни между ответами), чтобы я мог продолжать работать над этим дальше. - person jfriend00; 15.09.2015