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

1

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

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

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

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

Сначала немного предыстории.

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

Первоначально бот был написан кем-то другим, и их решение для добавления и удаления объектов в/из каждой коллекции должно было иметь четыре отдельные команды: "! Insultadd", "! Insultdelete", "! Jokeadd" и "! Jokedelete". Моя первая мысль, увидев это, была "модульность, съешь свое сердце. Yikes". Кодовая база содержала много повторяющихся кодов, как это, и поэтому я поставил своей целью абстрагировать функциональность настолько, чтобы можно было устранить большую часть этой избыточности, и в целом кодовую базу было бы намного проще расширять и поддерживать.

Итак, я придумал команду под названием "! Db". Уже есть слой модульности: все, что делает! Db, это вызывает "подкоманды", которые реализуют каждую отдельную функцию. Эти подкоманды называются такими вещами, как "! Dbadd", "! Dbdelete" и т.д., И они не предназначены для самостоятельного вызова. Важно отметить, что я сначала написал эти подкоманды, и только после того, как все они были независимо работоспособны, я создал! Db, чтобы упростить их упаковку, просто используя оператор case. (Например, вызов !dbadd !db add insultsCollection "ugly" (где insultsCollection - это набор оскорбительных прилагательных) в конечном итоге просто !dbadd с соответствующими аргументами.) Итак, изначально каждая !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, например, " beQuiet ", чтобы определить, печатает ли он свой материал или нет. Если бы это была "нормальная" кодовая база, то, вероятно, я бы так и сделал. Но...

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

Так что я не могу передать флаг. Хорошо что дальше? "Ну, - подумал я, - почему бы мне просто не переместить печать в !db?" Я так и сделал. Мой оператор switch-case теперь выглядит так:

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() фактически возвращает Promise! И оказывается, это решает одну из проблем:

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 если! Db не должен распечатай что угодно !! Итак, обе проблемы теперь решены! Спасибо всем за быстрый ответ! Вы были действительно полезны! <3

  • 0
    У вас нет никакого способа узнать, dbadd.execute() ли dbadd.execute() ?
  • 0
    Кроме того, я понимаю, что вы хотите, чтобы интерфейс был таким же для поведения по умолчанию, но вы можете изменить код для dbadd.execute() ?
Показать ещё 5 комментариев
Теги:
asynchronous
promise

2 ответа

1
Лучший ответ

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

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

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

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

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

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

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

  • 0
    Оказывается, поскольку execute() является async , он действительно возвращает Promise! (Спасибо @ Марк-Мейер!) Это означает, что я могу await этого! Я только что проверил это, и он ведет себя правильно, с некоторыми изменениями, которые я подробно описал в своем редактировании вопроса. Спасибо за вашу помощь!
0
  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
}
  1. Когда вы будете бороться с асинхронной логикой, лучше конвертировать все обратные вызовы в обещания, тогда вы будете чувствовать себя лучше. Например, dbAddSlient может использовать mongodriver и async с обратными вызовами и убедиться, что логика завершена после 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
    })
})()
  1. Проблема, с которой вы сталкиваетесь, заключается в том, что вы не привыкли к асинхронной логике.

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню