Предложил новый синтаксис await [timeoutInMs] для Javascript для TC39

Мы все знаем об использовании await of a промис: он в основном приказывает коду ждать, пока обещание разрешится или отклонится ... но подождать до когда? На самом деле бесконечно! На данный момент конечная судьба любого асинхронного кода, основанного на обещаниях, зависит от асинхронного источника.

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

Рассмотрим этот код:

let someReallyBigItemOnRAM = getSomeBulkyValue();
let res = await someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM);

Теперь в этом someReallyTimeConsumingAsyncFunction может потребоваться очень много времени, чтобы вернуться или сказать «Никогда не возвращаться» и навсегда оставить someReallyBigItemOnRAM в ОЗУ занятым в ОЗУ!

Чтобы решить эту проблему, разработчик JS должен иметь управление ожиданием. Новый код будет выглядеть примерно так:

let someReallyBigItemOnRAM = getSomeBulkyValue();
try{
let res = await[1500] someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM);
}catch(e){
  //try catch is used as await[timeInMs] can cause a timeoutError, which needs to be caught
  console.error(e);
}

Такое ожидание будет ждать не более 1500 мс, иначе будет генерироваться ошибка тайм-аута. ПРИМЕЧАНИЕ. Гарантируется, что при использовании без тайм-аута await будет вести себя точно так же, как и всегда, поэтому ни один старый код никогда не выйдет из строя из-за этого нового улучшения. Пользователь по-прежнему сможет использовать await без тайм-аута.

Теперь на ум приходит одно предложение - использовать Promise.race для имитации того, что здесь задумано:

let timeout = (time)=>new Promise(res=>setTimeout(res,time));
let someReallyBigItemOnRAM = getSomeBulkyValue();
let res = Promise.race([timeout(1500),someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM)]);

Но у Promise.race есть недостаток, который не соответствует требованиям. Хотя он проигнорирует значение, возвращаемое функцией someReallyTimeConsumingAsyncFunction, если она не будет завершена до истечения времени ожидания, но не прервет свое выполнение. Действительно, ваш код никогда не завершится, и someReallyBigItemOnRAM не будет выпущен до тех пор, пока не будет выполнено обещание someReallyTimeConsumingAsyncFunction. Теперь вы практически не можете контролировать someReallyBigItemOnRAM. Когда они хотят выпустить его, это зависит от асинхронного источника!

Асинхронность для циклов ожидания

Рассмотрим этот код:

for await(let s of anAsyncGeneratorOrStream){
//do some thing here
}
//once loop finish do shomething after wards

Опять же, anAsyncGeneratorOrStream имеет полную мощность, чтобы поддерживать этот цикл вечно без контроля разработчика. Поскольку источник является асинхронным, он может посылать данные с интервалом по своему усмотрению, и на выполнение может потребоваться вечность, если он захочет. Однако, если у нас есть синтаксис await[timeInMs] с обычным ожиданием:

try{
  for await[3000](let s of anAsyncGeneratorOrStream){
  //do some thing here
  }
}catch(e){
//catch time out error if above code throws it
}
//once loop finish do shomething after wards

Мы можем быть уверены, что выйдем из такого цикла максимум за 3000 миллисекунд. Намного лучший контроль в руках разработчика.

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

Почему это необходимо / важно?

  1. Лучший контроль со стороны разработчика, а не от функции asynchronouse.
  2. Может дать гораздо лучшее понимание того, сколько времени может занимать конкретная строка, и может помочь выявить узкие места в коде.
  3. Это очень просто реализовать, так как код просто генерирует ошибку тайм-аута. try/catch и async/await являются частью JS. await[timeInMs] является возможным источником ошибки тайм-аута, и, следовательно, компилятор может заранее предупредить пользователя о потенциальных точках тайм-аута в коде.

Какие опасения, и они действительно не должны волноваться

Аргумент: нельзя заставить код прерываться / прерываться между ними, это может вызвать потенциальную утечку ресурсов. Это некоторый ресурс, который должен был быть очищен, но был прерван из-за ошибки тайм-аута, будет в стадии утечки. Рассмотрим эту проблему (проблема 1):

async function doLongRunningTask() {
  const connection = await getConnectionFromPool()
  const { error, resource } = await connection.fetchResource()
  connection.release()
  if (error) throw error
  return resource
}

Если такой код будет прерван до того, как будет сделан вызов connection.release(), это всегда вызовет утечку.

await[3000] doLongRunningTask();//its feared that this new syntax can cause leaks inside long running task, as if it takes too long it will raise an error and will not get time to call connection.release()

Но следует отметить, что разработчик намеренно написал await[timeInMs], и пользователь знает, что это вызовет ошибку. Когда что-то является преднамеренным, все последствия не являются неожиданными, они являются предполагаемыми результатами.

Пользователь может создать такую ​​преднамеренную проблему, написав код как таковой для той же проблемы без использования await [timeInMs]: (пример 1)

//deliberate attempt to mess up some one's api code:
let t = getConnectionFromPool;
getConnectionFromPool = ()=>setTimeout(a=>throw "error",100); return t();
async function doLongRunningTask() {
  const connection = await getConnectionFromPool()
  const { error, resource } = await connection.fetchResource()
  connection.release()
  if (error) throw error
  return resource
}

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

async function doLongRunningTask() {
let connection;  
try{
 //now any where any exception occurs, or an interrupt exception is thrown, or time out error is throw in middle, all clean up will still take place.
  }catch(e){
     if(connection) connection.release();
  }
}

Они написали код, как обсуждалось в предыдущем примере (проблема 1), возможно, это именно то, что они хотели, поскольку это то, что делает код, который они написали! (Поскольку это позволяет людям все равно испортить это, даже если await [timeOutInMs] отсутствует на месте, как объяснено в примере 1).

Этот новый синтаксис действительно дает лучший контроль разработчику, чтобы он мог обернуть такой код с помощью try catch:

try{
await[3000] doLongRunningTask();//a try catch as this line can possible throw timeout error or any other error within from function even without timeout
}catch(e){
//you actually get a chance to clean up manually if something goes wrong.
}

Контекст

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

for await(let data of asyncStreamFromChannelOfConsensusResponse){
//collect thier response
}
//do something after response are recived.

Теперь эту проблему нельзя оставлять на усмотрение участника консенсуса: если никто не отправит никакого ответа, код будет работать вечно. Так возникла потребность в таком await с таймаутом.

Это довольно просто, очень ясно, что он собирается делать: await for x amount of timeat most === `await [x] somePromise. Люди всегда хотели контролировать отмену обещания, и это один из способов.

Я надеюсь, что другие люди сочтут это полезным и хорошей функцией в прекрасном Javascript!
- -
Предложение на форуме TC39: https://es.discourse.group/t/timeout-for- an-async-loop-if-loop-do-not-finishes-before-timeout-it-will-break-anyway / 1021
Вот предложение:
https: // github. com / anuragvohraec / offer-es-await-timeout

Итак, окончательный вердикт после обсуждения на форуме TC39 по этому вопросу:

Такой более совершенный обобщенный синтаксис уже находится в стадии разработки.

let someValue = await[cancelToken] getSomeValueFromPromise()

Использование токена отмены для решения проблемы тайм-аута:

let someValue = await[timeoutFunc(3000)] getSomeValueFromPromise();

Маркер отмены обеспечивает более общий подход к отмене обещания.

let someValue = await[createCustomCancelToken(someArgs)] getSomeValueFromPromise();

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