Как сделать, чтобы функция могла использовать как callback, так и async?

Имею библиотеку, которая поддерживает callback.

Вот пример функции для нее:

import fs from "fs";

removeFile(req, file, cb) {
    fs.unlink(file.path, cb);
};

По всему приложению использую async/await:

import {promises as fs} from "fs";

    async removeFile(filePath) {
        return await fs.unlink(filePath);
    };

Можно ли из этих двух сделать одну универсальною, с целью лаконичности?

Вот моя попытка, идентичны ли они, не сломаю ли я библиотеку, что работает на callback?

import {promises as fs} from "fs";
import fsCb from "fs";

    async removeFile(filePath, cb = false) {
        if (cb) {
            return fsCb.unlink(filePath, cb)
        }
        return await fs.unlink(filePath);
    };

Ответы (2 шт):

Автор решения: vsemozhebuty

Так как большинство функций с обратными вызовами ничего не возвращают, то вряд ли опциональный возврат Promise может что-то сломать, если и ваши функции с колбеками возвращали undefined.

Разве что return await обычно считается излишней работой: нет смысла дожидаться результата Promise, чтобы сразу опять облечь его в Promise (ведь любая асинхронная функция возвращает Promise). Достаточно будет return fs.unlink(filePath);. Тогда вам даже не нужно будет объявлять функцию как async (таким двойным функциям не обязательно быть async, если они не используют await: при отсутствии колбека, они могут просто возвращать new Promise(), если нет чего-то готового, возвращающего Promise).

Хотя, если подумать, есть один возможный способ что-то сломать: если раньше при отсутствии колбека возникала ошибка, а теперь её не будет — тогда пользователь, который будет работать со старым интерфейсом и случайно забудет колбек (но не будет ждать Promise), может не получить сообщения об ошибке и не сможет понять, что не так. Так что лучше, наверное, перестраховаться и выпустить новую версию библиотеки как semver major.

→ Ссылка
Автор решения: nörbörnën

Немного добавлю к ответу уважаемого vsemozhebuty.

Если бы мне пришлось писать современную библиотеку unicornlib, которую нужно было бы расширить таким вот применением callback-ов то я, наверное, сделал бы сиблинг-библиотеку unicornlib-legacy и там заворачивал бы асинхронные вызовы в вызовы колбеков.

Если совсем нет выбора, то не вмешивался бы в бизнес-логику самой функции. Сколько разного кода нужно дописать для поддержки "cb | promise" можно посмотреть на примере функции pipeline из библиотеки sharp.

Думаю, я бы пошёл через декорирование:

const { promises: fs } = require('fs');

function cbDecorator(fn) {
  return (...args) => {
    try {
      fn(...args);
    } catch (err) {
      console.error(err);
    }
  }
}

function fnDecorator(fn) {
  return (...args) => {
    let cb;
    if (typeof args[args.length - 1] === 'function') {
      cb = cbDecorator(args.pop());
    }
    return fn(...args).then((res) => {
      if (cb) {
        cb(null, res);
      }
      return res;
    }).catch((err) => {
      if (cb) {
        cb(err);
      }
      return Promise.reject(err);
    });
  };
}

async function removeFile(filePath) {
  return fs.unlink(filePath);
};

module.exports.removeFile = fnDecorator(removeFile);
→ Ссылка