Практика: асинхронные паттерны
Загрузка с кешем
const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
return cache.get(url);
}
const promise = fetch(url).then((r) => r.json());
cache.set(url, promise);
try {
return await promise;
} catch (error) {
cache.delete(url);
throw error;
}
}
Кешируется промис, а не результат. Два одновременных вызова — один запрос.
Ограничение параллельности
async function pool(items, limit, fn) {
const results = new Array(items.length);
let index = 0;
async function worker() {
while (index < items.length) {
const i = index++;
results[i] = await fn(items[i], i);
}
}
const workers = Array.from({ length: limit }, worker);
await Promise.all(workers);
return results;
}
await pool(urls, 5, (url) => fetch(url).then((r) => r.json()));
Пять одновременных запросов, остальные ждут.
Retry с backoff
async function retry(fn, { attempts = 3, baseDelay = 1000, maxDelay = 10000 } = {}) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i === attempts - 1) break;
const delay = Math.min(baseDelay * 2 ** i, maxDelay);
const jitter = Math.random() * delay * 0.1;
await sleep(delay + jitter);
}
}
throw lastError;
}
Jitter предотвращает «стадо» одновременных retry.
Circuit Breaker
При частых ошибках — прекратить попытки:
class CircuitBreaker {
constructor(fn, { threshold = 5, timeout = 60000 } = {}) {
this.fn = fn;
this.threshold = threshold;
this.timeout = timeout;
this.failures = 0;
this.state = 'closed';
this.nextAttempt = 0;
}
async call(...args) {
if (this.state === 'open') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit open');
}
this.state = 'half-open';
}
try {
const result = await this.fn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'closed';
}
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'open';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
Очередь задач
class TaskQueue {
constructor(limit = 1) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.next();
});
}
next() {
if (this.running >= this.limit || !this.queue.length) return;
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
}
}
const queue = new TaskQueue(3);
queue.add(() => fetchData(1));
queue.add(() => fetchData(2));
Дедупликация
Одинаковые запросы — один вызов:
const pending = new Map();
function dedupe(key, fn) {
if (pending.has(key)) {
return pending.get(key);
}
const promise = fn().finally(() => {
pending.delete(key);
});
pending.set(key, promise);
return promise;
}
dedupe('users', () => fetchUsers());
dedupe('users', () => fetchUsers()); // тот же промис
Отмена устаревших запросов
let controller = null;
async function search(query) {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
});
return await res.json();
} catch (error) {
if (error.name === 'AbortError') return null;
throw error;
}
}
Новый поиск отменяет старый.
Таймаут
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
),
]);
}
await withTimeout(fetchData(), 5000);
Прогресс
async function upload(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
onProgress(e.loaded / e.total);
}
};
xhr.onload = () => resolve(xhr.response);
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.open('POST', '/api/upload');
xhr.send(file);
});
}
await upload(file, (progress) => console.log(`${progress * 100}%`));
Итоги
- Кеш промисов для дедупликации
- Пул воркеров для ограничения
- Retry с backoff и jitter
- Circuit breaker для защиты
- Отмена старых запросов
withTimeoutдля ограничения по времени