Работа с асинхронным кодом

Библиотека Turf.js предоставляет набор чистых синхронных функций для геообработки: вычисление расстояний, буферов, пересечений, объединений и других операций над GeoJSON. Внутри самой библиотеки отсутствует асинхронность, поскольку алгоритмы являются детерминированными и работают с уже загруженными структурами данных. Асинхронность возникает на уровне интеграции: получение данных, обработка больших наборов, распределение вычислений и взаимодействие с внешними API.

Источники асинхронности в геообработке

Асинхронный код в контексте Turf.js появляется не из-за самой библиотеки, а из-за окружения:

  • загрузка GeoJSON с серверов или файловых систем;
  • обращение к геокодинг-API и тайловым сервисам;
  • обработка потоков данных (streaming);
  • выполнение тяжёлых вычислений вне основного потока (Web Workers / Worker Threads);
  • пакетная обработка больших массивов геометрий.

Каждый из этих сценариев требует управления конкурентностью и жизненным циклом данных до передачи их в Turf-функции.

Получение геоданных через fetch и подготовка к обработке

Типичный сценарий начинается с загрузки GeoJSON:

async function loadGeoJSON(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  return await response.json();
}

После получения данных они становятся входом для синхронных операций Turf.js:

import { buffer } fr om "@turf/turf";

async function process() {
  const data = await loadGeoJSON("/data/roads.geojson");

  const result = buffer(data, 0.5, { units: "kilometers" });

  return result;
}

Асинхронность здесь завершает свою роль до момента геообработки: Turf.js работает уже с готовым объектом.

Разделение I/O и CPU-нагрузки

Ключевой принцип при работе с Turf.js — отделение ввода-вывода от вычислений. I/O операции всегда асинхронны, а геометрические операции — синхронны и CPU-bound.

Пример комбинированного пайплайна:

async function pipeline() {
  const [roads, rivers] = await Promise.all([
    fetch("/roads.geojson").then(r => r.json()),
    fetch("/rivers.geojson").then(r => r.json())
  ]);

  const bufferedRivers = buffer(rivers, 1, { units: "kilometers" });

  return bufferedRivers;
}

Использование Promise.all уменьшает время ожидания загрузки и позволяет параллелизовать I/O.

Обработка больших наборов данных

При работе с крупными GeoJSON-файлами синхронные операции Turf.js могут блокировать основной поток. Асинхронность в этом случае используется для разбиения работы на части.

Батчевая обработка

async function processInBatches(features, batchSize) {
  const results = [];

  for (let i = 0; i < features.length; i += batchSize) {
    const batch = features.slice(i, i + batchSize);

    const processed = batch.map(feature =>
      buffer(feature, 0.2, { units: "kilometers" })
    );

    results.push(...processed);

    await new Promise(resolve => setTimeout(resolve, 0));
  }

  return results;
}

Задержка через setTimeout позволяет отдавать управление event loop и предотвращает “заморозку” интерфейса в браузере.

Использование Web Workers

Для тяжёлых геоопераций применяется вынос Turf.js в отдельный поток. Это особенно важно при:

  • пересечении больших полигонов;
  • обработке тысяч объектов;
  • построении сеток и кластеризации.

Worker-сценарий

// worker.js
importScripts("https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js");

self.onmess age = function (e) {
  const { feature, distance } = e.data;

  const result = turf.buffer(feature, distance, {
    units: "kilometers"
  });

  self.postMessage(result);
};

Основной поток

function runWorker(feature) {
  return new Promise((resolve) => {
    const worker = new Worker("worker.js");

    worker.onmess age = (e) => {
      resolve(e.data);
      worker.terminate();
    };

    worker.postMessage({
      feature,
      distance: 1
    });
  });
}

Асинхронность здесь реализуется через Promise, оборачивающий коммуникацию с worker.

Конвейеры обработки данных (async pipelines)

При сложной геообработке формируется цепочка этапов:

  1. загрузка данных;
  2. нормализация GeoJSON;
  3. фильтрация;
  4. геооперации Turf.js;
  5. агрегация результата;
  6. сохранение или отправка.
async function geoPipeline(url) {
  const data = await fetch(url).then(r => r.json());

  const filtered = data.features.filter(f => f.geometry !== null);

  const processed = filtered.map(f =>
    buffer(f, 0.3, { units: "kilometers" })
  );

  return {
    type: "FeatureCollection",
    features: processed
  };
}

Каждый этап может быть изолирован и потенциально вынесен в отдельный асинхронный шаг.

Параллелизация геоопераций

Хотя Turf.js не предоставляет встроенной параллельной обработки, асинхронность позволяет организовать конкурентное выполнение через:

  • Promise.all
  • пул воркеров
  • ограничение concurrency

Простейшая параллельная обработка

async function parallelBuffer(features) {
  const tasks = features.map(feature =>
    Promise.resolve(buffer(feature, 0.5, { units: "kilometers" }))
  );

  return await Promise.all(tasks);
}

Такой подход эффективен только при умеренных объёмах данных, так как синхронная CPU-нагрузка всё равно выполняется в основном потоке.

Контроль конкурентности

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

async function mapWithLimit(items, lim it, fn) {
  const results = [];
  let index = 0;

  const workers = Array.from({ length: limit }, async () => {
    while (index < items.length) {
      const current = index++;
      results[current] = await fn(items[current]);
    }
  });

  await Promise.all(workers);
  return results;
}

Использование этого подхода позволяет равномерно распределить нагрузку при комбинировании Turf.js с асинхронными операциями.

Асинхронная интеграция с потоковыми данными

В сценариях GIS-платформ данные могут поступать потоками: например, через WebSocket или серверные события.

async function handleStream(stream) {
  for await (const chunk of stream) {
    const geo = JSON.parse(chunk);

    const enriched = buffer(geo, 0.1, {
      units: "kilometers"
    });

    console.log(enriched);
  }
}

Асинхронный итератор позволяет обрабатывать данные по мере поступления, не дожидаясь завершения потока.

Сочетание Turf.js с асинхронными API геосервисов

Часто Turf.js используется вместе с внешними сервисами:

  • геокодинг;
  • маршрутизация;
  • получение тайлов;
  • пространственные индексы.
async function enrichFeature(feature) {
  const coords = feature.geometry.coordinates;

  const response = await fetch(
    `https://api.example.com/geocode?lon=${coords[0]}&lat=${coords[1]}`
  );

  const meta = await response.json();

  return {
    ...feature,
    properties: {
      ...feature.properties,
      ...meta
    }
  };
}

После обогащения данных выполняются Turf-операции:

const enriched = await enrichFeature(feature);
const result = buffer(enriched, 0.5, { units: "kilometers" });

Типичные ошибки при смешивании async и Turf.js

Потеря контроля над синхронной нагрузкой

Оборачивание map в Promise.all не делает вычисления параллельными — они всё равно выполняются в главном потоке.

Избыточная асинхронность

await Promise.resolve(buffer(feature, 1));

Такой код не даёт преимуществ и только усложняет логику.

Блокировка event loop

Большие коллекции GeoJSON без батчинга могут приводить к зависанию интерфейса даже при использовании async/await.

Архитектурные паттерны

Producer–Consumer

  • Producer: асинхронная загрузка данных
  • Consumer: синхронная обработка Turf.js в воркерах

Pipeline

Чёткое разделение стадий обработки с возможностью масштабирования отдельных этапов.

Worker Pool

Фиксированное количество воркеров, распределяющих Turf-операции.

Итоговая модель взаимодействия

Асинхронность в экосистеме Turf.js является внешним слоем управления данными и вычислениями. Библиотека остаётся чистым синхронным ядром геоалгоритмов, а асинхронные конструкции JavaScript используются для:

  • управления загрузкой и передачей данных;
  • разбиения вычислений на части;
  • масштабирования через Web Workers;
  • интеграции с внешними сервисами;
  • предотвращения блокировки основного потока.