Как «ожидать» (или .then ()) функцию, которая является асинхронной, но заключена в модуль и не возвращает обещание - PullRequest
0 голосов
/ 30 ноября 2018

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

(Правка: Этого не произошло. Я до сих пор не знаю, что делать. Надеюсь, не существует правила сайта против таких гигантских вопросов, как этот ...... здесь *)

Я пишу код для бота Discord (использующего Node и Discord.js), который взаимодействует с базой данных.(В частности MongoDB.) Конечно, это означает double асинхронное поведение.Когда я пишу вещи самым простым способом, все работает довольно хорошо, и я думаю, что в целом я достаточно хорошо понимаю Обещания, обратные вызовы и await, чтобы я мог гарантировать, что все происходит в правильной последовательности.

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

Во-первых, немного фона.

У бота есть ряд команд, которые используют базу данных;мы назовем их «! оскорбление» и «! шутка».Идея этих команд заключается в том, что они процедурно составляют оскорбление или шутку, которые составлены из компонентов, которые пользователи добавили в базу данных.У каждой команды есть отдельная «коллекция» (термин MongoDB, думаю, таблица SQL), содержащая их соответствующие данные, которые были введены пользователями.

Бот изначально был написан кем-то другим, и его решение для добавления и удаления объектов в/ из каждой коллекции должно было быть четыре отдельные команды: "! insultadd", "! insultdelete", "! jokeadd" и "! jokedelete".Моя первая мысль, увидев это, была «модульность, съешь свое сердце. Yikes».Кодовая база содержала большое количество повторяющихся кодов, как это, и поэтому я поставил своей целью абстрагировать функциональность настолько, чтобы можно было устранить большую часть этой избыточности, и в целом кодовую базу было бы намного проще расширять и поддерживать.

ИтакЯ пришел с командой "! Db".Уже есть слой модульности: все, что делает! Db, это вызывает «подкоманды», которые реализуют каждую отдельную функцию.Эти подкоманды называются такими вещами, как «! Dbadd», «! Dbdelete» и т. Д., И они не предназначены для самостоятельного вызова.Важно отметить, что я сначала написал эти подкоманды, и только после того, как все они были независимо работоспособны, я создал! Db, чтобы обернуть их в упрощенной форме, просто используя оператор case.(Например, вызов !db add insultsCollection "ugly" (где insultsCollection - набор оскорбительных прилагательных) просто закончится вызовом !dbadd с соответствующими аргументами.) Итак, первоначально каждая подкоманда выводила результаты самостоятельно,используя строки типа msg.channel.send('Inserted "' + selectedItem + '" into ' + selectedCollection + '.');.

Первоначально это работало просто отлично.! db не нужно было делать ничего больше, чем просто:

var dbadd = require('../commandsInternal/dbadd.js');
dbadd.execute(msg,args.slice(1),db);

и! dbadd позаботился бы о том, чтобы распечатать пользователю, что операция прошла успешно, сообщив, какой элемент был вставлен в БД.

Однако, важной частью этого гигантского рефакторинга является то, что внешнее поведение и использование остаются в основном одинаковыми для конечного пользователя - то есть! Jokeadd и его родственники останутся, но их внутренние части будут вычерпаны и замененыс вызовами соответствующих функций! дБ.Здесь мы начинаем сталкиваться с неприятностями.Когда я пытаюсь вызвать что-то вроде! Insultadd, это произойдет:

> !insultadd "ugly"
Inserted "ugly" into "insultsCollection". (This is printed by !dbadd.)
The bot can now call you "ugly"! (This is printed by !insultadd.)

Это поведение нежелательно, потому что в основном мы хотим представить пользователю, как если бы это был простой список прилагательных, и поэтому мы хотим избежать ссылок, например, на имена коллекций в БД.Итак, как я это исправил?Я думаю, что наиболее распространенным способом было бы добавить какой-либо флаг к подкомандам, например "beQuiet", чтобы определить, печатает ли он свои собственные вещи или нет.Если бы это была «нормальная» кодовая база, я бы, наверное, так и сделал.Но ...

Команды написаны в модулях Node, которые экспортируют несколько вещей: имя команды, время восстановления команды и т. Д., Но что наиболее важно, функцию с именем execute(msg, args, db),Эта функция - то, как основной поток бота вызывает произвольные команды.Он ищет имя команды, сопоставляет его с объектом, а затем пытается выполнить метод execute для объекта command.Обратите внимание, что execute принимает три аргумента ... объект Discord.js Message, аргументы команды (массив строк) и объект MongoDB Db.Чтобы передать флаг типа "beQuiet" в! Dbadd, я был бы вынужден добавить еще один аргумент к execute, что мне крайне не хотелось бы делать, потому что это означало бы, что некоторые командыполучить "специальные" аргументы по причинам, и ... тьфу.Это было бы нарушением последовательности, предлагая вещи стать полностью свободными для всех.

Так что я не могу передать флаг.Хорошо что дальше?«Ну, - подумал я, - почему бы мне просто не переместить печать в !db?»Я так и сделал.Теперь мое заявление о переключении выглядит следующим образом:

switch (choice) {
case "add":
    dbadd.execute(msg,args.slice(1),db);
    msg.channel.send('Inserted "' + args[2] + '" into ' + args[1] + '.');
    break;
case "delete":
    dbdelete.execute(msg,args.slice(1),db);
    msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.');
    break;
// ... etc
}

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

> !db delete insultsCollection asdfasdf
Did the user give a collection that exists? : true (Debugging output)
Error: No matches in given collection. (Correct error output from !dbdelete)
"asda" has been removed from hugs. (Erroneous output from !db)

Э-э-э.Итак, почему это происходит?По сути, это из-за асинхронности.Все вызовы в базу данных требуют от вас либо обратного вызова, либо обработки обещания.(Я предпочитаю последнее, когда это возможно.) Итак, у! Dbdelete есть такие вещи:

var query = { value: { $eq: selectedItem} };
let numOfFind = await db.collection(selectedCollection)
                        .find(query)
                        .count();
// Note that .count() returns a Promise that resolves to an int.
// Hence the await.

if (numOfFind == 0) {
    msg.channel.send("Error: No matches in given collection.");
    return;
}

Удобно, верно?Превращение функции execute() (в которую входит приведенный выше код) в функцию async сделало все намного проще для написания.Я использую .then(), где это уместно, и все в порядке.Но проблема, по сути, заключается в том, что return ...

(Ой, на минуту я подумала, что сама себя подтрунила в решении проблемы. Но, очевидно, просто добавление throw не работает.)

Хорошо, так ... проблема в том ... использую ли я return или throw,! Db не волнует.То, как я об этом думаю, выполнение асинхронного вызова функции (например, db.collection (). Find ()) приводит к запуску независимой «работы».(Я уверен, что я очень ошибаюсь по этому поводу, но эта модель мышления сработала до сих пор.) Видя, что такие вещи, как:

db.collection(selectedCollection).deleteMany(query, function(err, result) {
    if (err) {
        throw err;

        console.log('Something went wrong!');
        return;
    }
    console.log('"' + selectedItem + '" has been removed from ' + selectedCollection + '.');
});
console.log("Success! Deleted the thing.");

на самом деле будут печатать "Успех!"ПЕРЕД фактическим удалением элемента, я пришел к выводу, что сценарий продолжает веселиться, когда вы вызываете что-то асинхронное, и если вы хотите, чтобы это было на самом деле , то напечатайте это впоследствии, вынужно (в случае выше) поместить его в функцию обратного вызова, или использовать .then(), или await результат.У вас есть до.

Но проблема в ... из-за модульности! Dbdelete, я не могу ничего из этого сделать.Они не работают:

// Option 1: Callbacks.
// Doesn't work because execute() doesn't take a callback!
case "delete":
    dbdelete.execute(msg,args.slice(1),db, function(err, result) {
        msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    });
    break;

// Option 2: .then().
// Doesn't work because execute() doesn't return a Promise!
case "delete":
    dbdelete.execute(msg,args.slice(1),db)
    .then(function(err, result) {
        msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    });
    break;

// Option 3: await.
// Doesn't work because... I don't really know why but I know it doesn't work.
// Also, again, execute() doesn't return a promise so we can't await it.
case "delete":
    await dbdelete.execute(msg,args.slice(1),db);
    msg.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.',msg);
    break;

Итак, я в конце своей веревки.Я понятия не имею, как решить это.Честно говоря, я серьезно подумываю о том, чтобы заставить .execute () возвращать Promise, чтобы я мог .then ().Но я действительно не хочу этого делать, тем более что я не знаю как. Короче говоря: есть ли любой способ сделать .then () для функции, которая невернуть обещание?Если бы я мог просто сделать это блокировкой, мы были бы в порядке.

ОБНОВЛЕНИЕ: Вот код для dbdelete.js: https://pastebin.com/LdHm3ybU ОБНОВЛЕНИЕ 2: По словам Марка Мейера, поскольку я использовал ключевое слово await, execute() на самом деле действительно возвращает обещание!И оказывается, это решает одну из проблем:

case "delete":
    let throwaway = await dbdelete.execute(msg,args.slice(1),db);
    message.channel.send('"' + args[2] + '" has been removed from ' + args[1] + '.');
    break;

Этот код приводит к более близкому к предполагаемому результату: оператор print по-прежнему всегда выполняется даже при ошибке, но ... тогда я просто заставляю dbdelete.execute() возвращать логическое значение, которое я установил в false if! Dbне должен ничего печатать !!Итак, обе проблемы теперь решены!Спасибо всем за столь быстрый ответ!Вы были действительно полезны!<3 </p>

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018
  1. Я предпочитаю "beQuiet" таким образом, повторное использование другой асинхронной логики более или менее аналогично

const dbAddSlient = async(..args) => {
  //your db insertion
  return result
}

const dbAdd = async(...args) => {
  //you can use try/catch to wrap your async logic
  const result = await dbAddSlient(...args) //reuse
  console.log('your log') //additional opts
  return result
}

module.exports = {
  dbAddSlient,
  dbAdd
}
Когда вы будете бороться с асинхронной логикой, лучше конвертировать все обратные вызовы в обещания, тогда вы будете чувствовать себя лучше.Например, dbAddSlient может использовать mongodriver и асинхронную логику с обратными вызовами, а также убедиться, что логика завершена после await или then

const dbinert = (data) => new Promise((resolve, reject) => {
  MongoClient.connect("mongodb://localhost:27017/integration_tests", function(err, db) {
    if (err) {
      reject(err)
    }
    db.collection('mongoclient_test').insert(data, function(err, result) {
      if (err) {
        reject(err)
      }
      db.close()
      resolve(result)
    })
  })
})


const dbAddSlient = async(..args) => {
  const result = await dbinert(somedata)
  //result is what you resolved, and now all the db operation is surely done
  return result
}

// some chained logic with async
(async() => {
  try {
    await someAsync1()
    const result = await dbAddSlient(data)
    await someAsync3()
  } catch (e) {
    //handle error
  }
})()

// or use promise
(() => {
  someAsync1()
    .then(() => dbAddSlient(data))
    .then((result) => someAsync3())
    .catch(e => {
      //handle error
    })
})()
Проблема, с которой вы сталкиваетесь, заключается в том, что вы не привыкли к асинхронной логике. Предлагаю прочитать статью, лучше потренироваться

https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8

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

Promise.all https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promise.race https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

генератор https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator

Если у вас возникли вопросы, просто прокомментируйте этот ответ

0 голосов
/ 30 ноября 2018

Если ваш .execute() метод является асинхронным, то ЕДИНСТВЕННЫЙ способ, которым вызывающая сторона может знать, когда он завершен, или узнать, каково его возвращаемое значение, если вы разрабатываете API и асинхронный механизм для того, чтобы знать это.Синхронная функция будет возвращаться задолго до того, как будет выполнена асинхронная операция внутри функции, поэтому вызывающая сторона не может знать, когда она выполнена, или не знает, какого результата она достигла.

Итак, вам необходимо создать механизм для вызывающей стороны.знать, когда .execute() сделано и каков его результат.Общие механизмы:

  1. Возвращает обещание, которое разрешается / отклоняется с окончательным результатом.Вызывающий абонент использует .then() или await для его отслеживания.

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

  3. Используйте какой-то другой механизм, например, событие, которое запускается на каком-то известном объекте (потоки используют эту схему).

Вам нужно будет либо найти какой-нибудь известный объект, который звонящий уже знает.что вы можете запустить событие или вам нужно будет изменить API, чтобы иметь асинхронный интерфейс.В Javascript нет способа преобразовать асинхронную операцию в синхронное возвращаемое значение, поэтому вам нужно будет изменить интерфейс.

Для результата, возвращаемого за один раз (не какое-либо текущее событие, которое запускается несколько раз),«Современный» способ сделать что-то в Javascript - это вернуть обещание, и тогда вызывающая сторона может использовать .then() или await для этого обещания.

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