Обработка Promise и отмена операций

Все основные операции в esbuild построены вокруг асинхронной модели. Функции build() и transform() возвращают Promise, что делает библиотеку совместимой с современными механизмами обработки асинхронности в JavaScript и позволяет интегрировать её в серверные и CLI-пайплайны без блокировки потока выполнения.

Асинхронный результат сборки

Функция сборки имеет следующий базовый контракт:

import * as esbuild from "esbuild";

const result = await esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
});

Promise резолвится объектом результата, содержащим:

  • outputFiles (при использовании write: false)
  • metafile (при включённой опции metafile: true)
  • warnings и errors
  • служебную информацию о сборке

Такой подход позволяет строить цепочки обработки результата:

esbuild.build(options)
  .then(result => {
    if (result.warnings.length) {
      console.warn(result.warnings);
    }
    return result;
  })
  .then(result => {
    // дальнейшая обработка метаданных
  })
  .catch(err => {
    console.error(err);
  });

Promise-ориентированная архитектура делает esbuild удобным для интеграции в системы, где требуется последовательная обработка стадий сборки.


Ошибки и отклонение Promise

Ошибки в esbuild классифицируются на две группы:

  • Ошибки конфигурации и компиляции (rejected Promise)
  • Предупреждения (не прерывают выполнение)

Если сборка не может быть завершена, Promise отклоняется:

try {
  await esbuild.build({
    entryPoints: ["src/index.js"],
    bundle: true,
    outfile: "dist/bundle.js",
  });
} catch (e) {
  console.error("Ошибка сборки:", e);
}

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


Promise в плагинах esbuild

Плагинная система esbuild полностью асинхронна. Все хуки могут возвращать Promise, что позволяет выполнять операции ввода-вывода без блокировки сборки.

const plugin = {
  name: "async-loader",
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const content = await fs.promises.readFile(args.path, "utf8");

      return {
        contents: content,
        loader: "text",
      };
    });
  },
};

Асинхронность в onResolve и onLoad обеспечивает:

  • загрузку файлов из сети или базы данных
  • динамическое преобразование контента
  • интеграцию с удалёнными API

Каждый такой хук может возвращать Promise, и esbuild автоматически включает его в граф выполнения.


Прерывание и отмена операций сборки

С ростом размера проектов становится критически важной возможность остановки активной сборки. В esbuild существует несколько механизмов управления жизненным циклом процесса.

Контекст сборки (Context API)

В современных версиях esbuild используется API контекста:

const ctx = await esbuild.context({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
});

await ctx.rebuild();

Контекст позволяет:

  • выполнять повторные сборки без пересоздания окружения
  • управлять жизненным циклом сборщика
  • освобождать ресурсы

Освобождение контекста:

await ctx.dispose();

dispose() завершает активные операции и освобождает внутренние ресурсы. После вызова контекст становится невалидным для дальнейших сборок.


Отмена активной сборки

Механизм отмены зависит от режима работы:

1. Отмена через AbortSignal

В некоторых конфигурациях поддерживается передача AbortSignal, позволяющая прервать выполнение:

const controller = new AbortController();

esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
  signal: controller.signal,
});

controller.abort();

При вызове abort() текущая операция завершается с ошибкой отмены, Promise отклоняется.


2. Отмена через контекст

При использовании контекста отмена выполняется через его методы управления:

const ctx = await esbuild.context({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
});

ctx.dispose();

В отличие от AbortSignal, dispose() ориентирован не только на текущую операцию, но и на полное завершение жизненного цикла сборщика.


Поведение incremental build и отмена

Инкрементальная сборка изменяет модель выполнения: esbuild сохраняет состояние графа модулей между вызовами.

const ctx = await esbuild.context({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
});

await ctx.rebuild();
await ctx.rebuild();

Если в момент выполнения rebuild() инициируется новая операция или вызывается dispose(), текущий процесс может быть прерван. В таких сценариях важно учитывать:

  • предыдущие Promise могут быть отклонены
  • состояние файлового графа может обновляться асинхронно
  • гонки между rebuild и dispose возможны при параллельных вызовах

Конкурентность и гонки Promise

При работе с esbuild часто возникает ситуация параллельных сборок:

const build1 = esbuild.build(options);
const build2 = esbuild.build(options);

Каждая операция независима, но потребляет системные ресурсы. При высокой частоте вызовов возможны:

  • рост потребления памяти
  • конкуренция за файловые дескрипторы
  • задержки в планировании задач

Контекстный API снижает риск таких гонок, так как сериализует операции внутри одного окружения.


Управление ошибками при отмене

Отмена операции не всегда интерпретируется как критическая ошибка. В практике обработки Promise используется разделение типов ошибок:

try {
  await ctx.rebuild();
} catch (e) {
  if (e.name === "CanceledError") {
    // нормальное завершение при отмене
  } else {
    throw e;
  }
}

Такая модель позволяет различать:

  • ошибки компиляции
  • ошибки окружения
  • штатную отмену выполнения

Асинхронные плагины и отмена задач

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

build.onLoad({ filter: /\.json$/ }, async (args) => {
  const data = await fetchData(args.path);

  return {
    contents: JSON.stringify(data),
    loader: "json",
  };
});

Если сборка была отменена во время fetchData, результат может быть проигнорирован, но сама операция может продолжить выполнение. Это накладывает требования на:

  • контроль побочных эффектов
  • использование AbortSignal внутри пользовательских операций
  • избегание неидемпотентных действий

Согласование Promise-цепочек в сложных сборках

В сложных конфигурациях с несколькими плагинами и асинхронными источниками данных возникает необходимость координации Promise:

  • порядок разрешения onResolve и onLoad
  • конкуренция между плагинами
  • каскадные зависимости модулей

esbuild гарантирует детерминированность графа модулей, но не гарантирует порядок выполнения пользовательских асинхронных операций внутри плагинов, если они не синхронизированы явно.


Практика безопасной отмены

Устойчивые схемы работы с отменой строятся на комбинации:

  • AbortSignal внутри внешних API-запросов
  • context.dispose() для завершения сборки
  • отслеживание состояния активных операций
  • минимизация побочных эффектов в асинхронных хук-функциях

Такая модель позволяет избежать утечек ресурсов и неконтролируемого выполнения Promise после завершения сборки.