feat(clients/weather): in-memory cache

This commit is contained in:
mikhail "synzr" 2025-12-02 16:32:33 +05:00
parent 5c0a84b3eb
commit 2e029e47c0
3 changed files with 61 additions and 22 deletions

View file

@ -26,6 +26,7 @@
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-svelte": "^3.13.0", "eslint-plugin-svelte": "^3.13.0",
"globals": "^16.5.0", "globals": "^16.5.0",
"node-cache": "^5.1.2",
"svelte": "^5.43.8", "svelte": "^5.43.8",
"svelte-check": "^4.3.4", "svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",

17
pnpm-lock.yaml generated
View file

@ -47,6 +47,9 @@ importers:
globals: globals:
specifier: ^16.5.0 specifier: ^16.5.0
version: 16.5.0 version: 16.5.0
node-cache:
specifier: ^5.1.2
version: 5.1.2
svelte: svelte:
specifier: ^5.43.8 specifier: ^5.43.8
version: 5.45.2 version: 5.45.2
@ -679,6 +682,10 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} 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: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1034,6 +1041,10 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-cache@5.1.2:
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
engines: {node: '>= 8.0.0'}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1801,6 +1812,8 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
clone@2.1.2: {}
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
@ -2133,6 +2146,10 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-cache@5.1.2:
dependencies:
clone: 2.1.2
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4

View file

@ -1,3 +1,4 @@
import NodeCache from "node-cache";
import crypto from "node:crypto"; 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"; const WEATHER_TOKEN_SECRET = "gravisfrontem";
/**
* Response cache.
*/
const cache = new NodeCache({
// NOTE: 10 minutes in a seconds
stdTTL: 60 * 10,
});
interface YandexWeatherData { interface YandexWeatherData {
/** /**
* Message from the server. * Message from the server.
@ -18,54 +27,54 @@ interface YandexWeatherData {
info: { info: {
/** /**
* URL of the location page on Yandex Weather * URL of the location page on Yandex Weather.
**/ **/
url: string; url: string;
}; };
fact: { fact: {
/** /**
* Current air temperature in °C * Current air temperature in °C.
**/ **/
temp: number; temp: number;
/** /**
* Water temperature in °C (may be missing) * Water temperature in °C.
**/ **/
temp_water?: number; temp_water?: number;
/** /**
* Perceived (feels like) temperature in °C * Perceived (feels like) temperature in °C.
**/ **/
feels_like: number; feels_like: number;
/** /**
* Weather condition code (e.g., "clear", "rain") * Weather condition code (e.g., "clear", "rain").
**/ **/
condition: string; condition: string;
/** /**
* Wind speed in m/s * Wind speed in m/s.
**/ **/
wind_speed: number; wind_speed: number;
/** /**
* Wind gust speed in m/s (may be missing) * Wind gust speed in m/s.
**/ **/
wind_gust?: number; wind_gust?: number;
/** /**
* Wind direction code (e.g., "nw", "s") * Wind direction.
**/ **/
wind_dir: string; wind_dir: string;
/** /**
* Atmospheric pressure in mmHg * Atmospheric pressure in mmHg.
**/ **/
pressure_mm: number; pressure_mm: number;
/** /**
* Air humidity percentage * Air humidity percentage.
**/ **/
humidity: number; humidity: number;
}; };
@ -111,7 +120,7 @@ export interface FetchWeatherResult {
gust?: number; gust?: number;
/** /**
* Wind direction code. * Wind direction.
**/ **/
direction: string; direction: string;
}; };
@ -140,6 +149,10 @@ function getWeatherHeaders() {
} }
export default async function fetchWeather(lat: number, lon: number): Promise<FetchWeatherResult> { export default async function fetchWeather(lat: number, lon: number): Promise<FetchWeatherResult> {
const key = `${lat};${lon}`
let result: YandexWeatherData | undefined = cache.get(key);
if (!result) {
const response = await fetch( const response = await fetch(
`https://api.weather.yandex.ru/v2/informers?lat=${lat}&lon=${lon}&lang=en_US`, `https://api.weather.yandex.ru/v2/informers?lat=${lat}&lon=${lon}&lang=en_US`,
{ {
@ -150,10 +163,18 @@ export default async function fetchWeather(lat: number, lon: number): Promise<Fe
} }
); );
const result: YandexWeatherData = await response.json(); result = await response.json();
// NOTE: to fix with typescript errors
if (!result) {
throw new Error('Result is undefined')
}
if (!response.ok) { if (!response.ok) {
throw new Error(`Yandex Weather API error: ${result.message}`) throw new Error(result.message!)
}
cache.set(key, result);
} }
return { return {