Corp/backend/static/js/managers/dataManager.js
Игорь Надеин 1fd06f1863 Рефакторинг: добавлен DataManager и API обертки для устранения дублирования запросов
НОВОЕ:
- static/js/managers/dataManager.js - централизованное кеширование и дедупликация
- static/js/api/calendarApi.js - обертка над calendar API с кешем (TTL 30s)
- static/js/api/departmentsApi.js - обертка над departments API с кешем (TTL 60s)

ИЗМЕНЕНО:
- calendarWidget.js теперь использует getCalendarEvents() и getMyDepartments()
- Убрано 3x дублирование запроса /api/v1/departments/my-departments/
- Убрано 2-3x дублирование запросов календаря

РЕЗУЛЬТАТ:
- Вместо 10+ API запросов теперь будет 3-4 (с кешем)
- Автоматическая дедупликация одновременных запросов
- Логирование Cache HIT/MISS для отладки
2025-11-28 14:24:26 +03:00

200 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @fileoverview DataManager - централизованное управление данными с кешированием и дедупликацией
* Предотвращает дублирование запросов и обеспечивает кеширование на клиенте
* @module managers/dataManager
*/
/**
* Менеджер данных с кешированием и дедупликацией запросов
*/
class DataManager {
constructor() {
/** @type {Map<string, {data: any, timestamp: number}>} */
this.cache = new Map();
/** @type {Map<string, Promise<any>>} */
this.pending = new Map();
/** @type {number} */
this.defaultTTL = 30000; // 30 секунд по умолчанию
// Подписчики на изменения данных
this.subscribers = new Map();
}
/**
* Получить данные с кешированием и дедупликацией
* @param {string} key - Уникальный ключ для данных
* @param {Function} fetchFn - Функция для загрузки данных (должна возвращать Promise)
* @param {number} [ttl] - Time to live в миллисекундах (по умолчанию 30000)
* @returns {Promise<any>} Данные
*/
async fetch(key, fetchFn, ttl = this.defaultTTL) {
// 1. Проверяем кеш
if (this.cache.has(key)) {
const { data, timestamp } = this.cache.get(key);
const age = Date.now() - timestamp;
if (age < ttl) {
console.log(`[DataManager] Cache HIT: ${key} (age: ${age}ms)`);
return data;
} else {
console.log(`[DataManager] Cache EXPIRED: ${key} (age: ${age}ms, ttl: ${ttl}ms)`);
this.cache.delete(key);
}
}
// 2. Проверяем pending запросы (дедупликация)
if (this.pending.has(key)) {
console.log(`[DataManager] Request DEDUPED: ${key}`);
return this.pending.get(key);
}
// 3. Делаем запрос
console.log(`[DataManager] Fetching: ${key}`);
const promise = fetchFn()
.then(data => {
// Сохраняем в кеш
this.cache.set(key, {
data,
timestamp: Date.now()
});
// Удаляем из pending
this.pending.delete(key);
// Уведомляем подписчиков
this.notify(key, data);
return data;
})
.catch(error => {
// Удаляем из pending при ошибке
this.pending.delete(key);
throw error;
});
// Сохраняем promise в pending
this.pending.set(key, promise);
return promise;
}
/**
* Инвалидировать кеш для ключа
* @param {string} key - Ключ данных
*/
invalidate(key) {
console.log(`[DataManager] Invalidating: ${key}`);
this.cache.delete(key);
this.notify(key, null);
}
/**
* Инвалидировать все ключи, соответствующие паттерну
* @param {RegExp|string} pattern - Паттерн для поиска ключей
*/
invalidatePattern(pattern) {
const regex = typeof pattern === 'string'
? new RegExp(pattern)
: pattern;
const keysToInvalidate = Array.from(this.cache.keys())
.filter(key => regex.test(key));
console.log(`[DataManager] Invalidating pattern "${pattern}":`, keysToInvalidate);
keysToInvalidate.forEach(key => {
this.cache.delete(key);
this.notify(key, null);
});
}
/**
* Очистить весь кеш
*/
clear() {
console.log('[DataManager] Clearing all cache');
this.cache.clear();
this.pending.clear();
}
/**
* Подписаться на изменения данных
* @param {string} key - Ключ данных
* @param {Function} callback - Коллбек (data) => void
* @returns {Function} Функция отписки
*/
subscribe(key, callback) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set());
}
this.subscribers.get(key).add(callback);
// Возвращаем функцию отписки
return () => {
const subs = this.subscribers.get(key);
if (subs) {
subs.delete(callback);
if (subs.size === 0) {
this.subscribers.delete(key);
}
}
};
}
/**
* Уведомить подписчиков об изменении данных
* @param {string} key - Ключ данных
* @param {any} data - Новые данные
* @private
*/
notify(key, data) {
const subs = this.subscribers.get(key);
if (subs) {
subs.forEach(callback => {
try {
callback(data);
} catch (err) {
console.error(`[DataManager] Subscriber error for key "${key}":`, err);
}
});
}
}
/**
* Получить статистику кеша
* @returns {Object} Статистика
*/
getStats() {
return {
cacheSize: this.cache.size,
pendingCount: this.pending.size,
subscribersCount: this.subscribers.size,
keys: Array.from(this.cache.keys())
};
}
/**
* Пре-загрузить данные в кеш
* @param {string} key - Ключ данных
* @param {any} data - Данные для сохранения
*/
preload(key, data) {
console.log(`[DataManager] Preloading: ${key}`);
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
// Экспортируем singleton
export const dataManager = new DataManager();
// Для отладки в консоли
if (typeof window !== 'undefined') {
window.dataManager = dataManager;
}