/** * @typedef {Object} MarkerData * @property {Blob} background - фон * @property {Array} images - изображения * @property {Array} slider - слайды */ /** Путь к php-прослойке, работающая с бд и внутренним хранилищем */ const db_get_filepath = '../db_get.php' /** Дефолтный срок годности кэша : 2 часа */ const CACHE_DEFAULT_TTL_MS = 2 * 60 * 60 * 1000; /** * Получить объект кэша, если поддерживается Cache API * @returns {Promise} */ async function getCache() { if (typeof caches !== 'undefined') { return await caches.open('cache'); } return null; } /** * Излечь данные из ответа * @param {string} type - тип извлекаемых данных * @param {Response} response - ответ * @returns {Promise} */ const extract_file = recovery(async (type, response) => { switch(type) { case 'file': return await response.blob(); case 'json': return await response.json(); default: throw new Error('Не определен type'); } }, (...e) => {throw Error(`Ошибки при извлечении (extract_file): ${e.join(', ')}`)} ); /** * @param {string} path - путь (либо локальный или https) * @param {object} options - параметры fetch * @param {int} timeout - секундомер */ const fetch_with_timeout = recovery(async (path, options = {}, timeout = 10 * 1000) => { const controller = new AbortController(); const signal = controller.signal; const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(path, {...options, signal}); try { return response; } finally { clearTimeout(timeoutId); } }, (e) => { if(e.name === 'AbortError') console.log('Запрос прерван по таймауту:', e); else console.log(e); }, (...e) => {throw Error(`Ошибки при запросе с таймером (fetch_with_timeout): ${e.join(', ')}`)} ); /** * Вернуть данные из ответа, кэшируя перед этим (если Cache API есть) * @param {string} type - тип извлекаемых данных * @param {string} path - путь (либо локальный или https) * @param {string} cache_key - ключ кэша * @param {Object} body_req - тело запроса * @returns {Promise} */ const return_fetch = recovery(async (type, path, cache_key, body_req) => { const cache = await getCache(); const response = !body_req ? await fetch_with_timeout(path) : await fetch_with_timeout(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body_req) }); if (cache) { const cloned = response.clone(); const blob = await cloned.blob(); const headers = new Headers(cloned.headers); headers.set('date', new Date().toUTCString()); await cache.put( cache_key, new Response(blob, { status: cloned.status, statusText: cloned.statusText, headers }) ); } return await extract_file(type, response); }, (...e) => {throw Error(`Ошибки при запросе и кэшировании (return_fetch): ${e.join(', ')}`)} ); /** * Получить данные из пути (локальный или https), с поддержкой кэша, если он есть * @param {string} type - тип извлекаемых данных * @param {string} path - путь (либо локальный или https) * @param {Object|null} body_req - тело запроса * @returns {Promise} */ const get = recovery(async (type, path, body_req = null) => { const cache = await getCache(); const cache_key = `${path}?type=${type}&file=${encodeURIComponent(JSON.stringify(body_req || {}))}`; if (cache) { const cached_response = await cache.match(cache_key); if (cached_response) { const expires_value = cached_response.headers.get('expires'); const date_value = cached_response.headers.get('date'); const exp = expires_value && expires_value !== '-1' ? Date.parse(expires_value) : Date.parse(date_value ?? '') + CACHE_DEFAULT_TTL_MS; if (Date.now() < exp) { return await extract_file(type, cached_response); } await cache.delete(cache_key); } } return await return_fetch(type, path, cache_key, body_req); }, (...e) => {throw Error(`Ошибки при получении (get): ${e.join(', ')}`)} ); /** * Получить данные на диске (абсолютный/относительный путь) * @param {string} filepath - путь к файлу * @returns {Promise} - без json */ const get_local = recovery(async (filepath) => { return await get('file', db_get_filepath, { mode: 'file', path: filepath }); }, (...e) => {throw Error(`Ошибки при локальном получении (get_local): ${e.join(', ')}`)} ); /** * Получить данные без кэширования * @param {string} type - тип извлекаемых данных * @param {string} path - путь (либо локальный или https) * @returns {Promise} */ const get_no_cached = recovery(async (type, path) => { const file = await fetch_with_timeout(path); return await extract_file(type, file); }, (...e) => {throw Error(`Ошибки при получении без кэширования (get_no_cached): ${e.join(', ')}`)} ); /** * Получить материалы поселения для инфо. страницы * @param {MarkerData} properties * @returns {Promise<{ background: Blob, images: Blob[], slider: Blob[]}>} */ const get_for_info_page = recovery(async (properties) => { const background = await get_local(properties.background); const images = await Promise.all( (properties.images || []).map(i_path => get_local(i_path)) ); const slider = Array.isArray(properties.slider) ? await Promise.all(properties.slider.map(s_path => get_local(s_path))) : []; return { background, images, slider, }; }, (...e) => {throw Error(`Ошибки при получении материалов для страницы (get_for_info_page): ${e.join(', ')}`)} ); /** * Получить иконки для стилизации страниц. * @returns {Promise} */ const get_icons = recovery(async () => await get_no_cached('json', './data/icons.json') , (...e) => {throw Error(`Ошибки при получении данных для стилизации (get_icons): ${e.join(', ')}`)} ); /** * Получить все geojson'ы из базы данных * @returns {Promise} */ const get_all_geojsons = recovery(async () => await get('json', db_get_filepath, {'mode': 'geojsons'}) , (...e) => {throw Error(`Ошибки при получении geojson'ов из бд (get_all_geojsons): ${e.join(', ')}`)} );