diff --git a/package.json b/package.json index 04fa0d8..a7d9ab4 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.0", "globals": "^16.5.0", + "node-cache": "^5.1.2", "svelte": "^5.43.8", "svelte-check": "^4.3.4", "tailwindcss": "^4.1.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a146fb..b99a37b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 + node-cache: + specifier: ^5.1.2 + version: 5.1.2 svelte: specifier: ^5.43.8 version: 5.45.2 @@ -679,6 +682,10 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1034,6 +1041,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1801,6 +1812,8 @@ snapshots: dependencies: clsx: 2.1.1 + clone@2.1.2: {} + clsx@2.1.1: {} color-convert@2.0.1: @@ -2133,6 +2146,10 @@ snapshots: natural-compare@1.4.0: {} + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/src/lib/clients/weather.ts b/src/lib/clients/weather.ts index 7a97052..c4088e8 100644 --- a/src/lib/clients/weather.ts +++ b/src/lib/clients/weather.ts @@ -1,3 +1,4 @@ +import NodeCache from "node-cache"; import crypto from "node:crypto"; /** @@ -10,6 +11,14 @@ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 */ const WEATHER_TOKEN_SECRET = "gravisfrontem"; +/** + * Response cache. + */ +const cache = new NodeCache({ + // NOTE: 10 minutes in a seconds + stdTTL: 60 * 10, +}); + interface YandexWeatherData { /** * Message from the server. @@ -18,54 +27,54 @@ interface YandexWeatherData { info: { /** - * URL of the location page on Yandex Weather + * URL of the location page on Yandex Weather. **/ url: string; }; fact: { /** - * Current air temperature in °C + * Current air temperature in °C. **/ temp: number; /** - * Water temperature in °C (may be missing) + * Water temperature in °C. **/ temp_water?: number; /** - * Perceived (“feels like”) temperature in °C + * Perceived (“feels like”) temperature in °C. **/ feels_like: number; /** - * Weather condition code (e.g., "clear", "rain") + * Weather condition code (e.g., "clear", "rain"). **/ condition: string; /** - * Wind speed in m/s + * Wind speed in m/s. **/ wind_speed: number; /** - * Wind gust speed in m/s (may be missing) + * Wind gust speed in m/s. **/ wind_gust?: number; /** - * Wind direction code (e.g., "nw", "s") + * Wind direction. **/ wind_dir: string; /** - * Atmospheric pressure in mmHg + * Atmospheric pressure in mmHg. **/ pressure_mm: number; /** - * Air humidity percentage + * Air humidity percentage. **/ humidity: number; }; @@ -111,7 +120,7 @@ export interface FetchWeatherResult { gust?: number; /** - * Wind direction code. + * Wind direction. **/ direction: string; }; @@ -140,20 +149,32 @@ function getWeatherHeaders() { } export default async function fetchWeather(lat: number, lon: number): Promise { - const response = await fetch( - `https://api.weather.yandex.ru/v2/informers?lat=${lat}&lon=${lon}&lang=en_US`, - { - headers: { - 'user-agent': USER_AGENT, - ...getWeatherHeaders(), - }, + const key = `${lat};${lon}` + + let result: YandexWeatherData | undefined = cache.get(key); + if (!result) { + const response = await fetch( + `https://api.weather.yandex.ru/v2/informers?lat=${lat}&lon=${lon}&lang=en_US`, + { + headers: { + 'user-agent': USER_AGENT, + ...getWeatherHeaders(), + }, + } + ); + + result = await response.json(); + + // NOTE: to fix with typescript errors + if (!result) { + throw new Error('Result is undefined') } - ); - const result: YandexWeatherData = await response.json(); + if (!response.ok) { + throw new Error(result.message!) + } - if (!response.ok) { - throw new Error(`Yandex Weather API error: ${result.message}`) + cache.set(key, result); } return {