diff --git a/README.md b/README.md index 3c015f5..7e52d7b 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ const weatherPlus = new WeatherPlus({ }); ``` -### Cache Time-to-Live (TTL) +#### Cache Time-to-Live (TTL) You can customize the cache TTL (time-to-live) in seconds. The default TTL is 300 seconds (5 minutes). @@ -169,6 +169,14 @@ const weatherPlus = new WeatherPlus({ }); ``` +#### Bypassing Cache + +You can bypass the cache and force a fresh request to the provider by setting the bypassCache option to true. + +```ts +const weather = await weatherPlus.getWeather(51.5074, -0.1278, { bypassCache: true }); +``` + ### Geohash Precision The library uses geohashing to cache weather data for nearby locations efficiently. Geohashing converts latitude and longitude into a short alphanumeric string, representing an area on the Earth’s surface. @@ -229,6 +237,7 @@ This library is built with TypeScript and includes type-safe interfaces for weat ```ts interface IWeatherData { + provider: string; temperature: { value: number; unit: string; @@ -248,6 +257,8 @@ interface IWeatherData { } ``` +Note today the response is fairly basic, but we're working on adding more data all of the time. + ### Complete Example ```ts diff --git a/package.json b/package.json index 4e93eac..dadd4a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weather-plus", - "version": "1.0.1", + "version": "1.0.2", "description": "Weather Plus is a powerful wrapper around various Weather APIs that simplifies adding weather data to your application", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/index.ts b/src/index.ts index 5faba4d..a9e1f68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { WeatherService } from './weatherService'; import { InvalidProviderLocationError, ProviderNotSupportedError, WeatherProviderError } from './errors'; import { RedisClientType } from 'redis'; +import { GetWeatherOptions } from './weatherService'; // Define the options interface for WeatherPlus interface WeatherPlusOptions { @@ -26,11 +27,11 @@ class WeatherPlus { } // Public method to get weather data for a given latitude and longitude - async getWeather(lat: number, lng: number) { - return this.weatherService.getWeather(lat, lng); + async getWeather(lat: number, lng: number, options?: GetWeatherOptions) { + return this.weatherService.getWeather(lat, lng, options); } } -export { WeatherService, InvalidProviderLocationError, ProviderNotSupportedError, WeatherProviderError }; +export { WeatherService, GetWeatherOptions, InvalidProviderLocationError, ProviderNotSupportedError, WeatherProviderError }; export * from './interfaces'; export default WeatherPlus; \ No newline at end of file diff --git a/src/weatherService.test.ts b/src/weatherService.test.ts index 0e1d8fc..ae9e1e6 100644 --- a/src/weatherService.test.ts +++ b/src/weatherService.test.ts @@ -1,4 +1,4 @@ -import { WeatherService } from './weatherService'; +import { WeatherService, GetWeatherOptions } from './weatherService'; import { InvalidProviderLocationError } from './errors'; import { IWeatherUnits, IWeatherData } from './interfaces'; import { IWeatherProvider } from './providers/IWeatherProvider'; @@ -275,4 +275,42 @@ describe('WeatherService', () => { expect(result).toEqual(mockWeatherData); expect(result.provider).toBe('openweather'); // Verify provider name }); + + it('should bypass cache when bypassCache option is true', async () => { + const mockCache = { + get: jest.fn(), + set: jest.fn(), + }; + + const mockProvider: IWeatherProvider = { + name: 'mockProvider', + getWeather: jest.fn().mockResolvedValue({ temperature: 25 }), + }; + + const weatherService = new WeatherService({ + providers: ['nws'], + apiKeys: {}, + redisClient: undefined, + }); + + // Inject mock cache and provider + (weatherService as any).cache = mockCache; + (weatherService as any).providers = [mockProvider]; + + // Call getWeather with bypassCache option + const options: GetWeatherOptions = { bypassCache: true }; + const result = await weatherService.getWeather(0, 0, options); + + // Expect cache.get not to be called + expect(mockCache.get).not.toHaveBeenCalled(); + + // Expect provider.getWeather to be called + expect(mockProvider.getWeather).toHaveBeenCalledWith(0, 0); + + // Expect cache.set to be called with new data + expect(mockCache.set).toHaveBeenCalledWith(expect.any(String), JSON.stringify(result)); + + // Verify the result + expect(result).toEqual({ temperature: 25 }); + }); }); \ No newline at end of file diff --git a/src/weatherService.ts b/src/weatherService.ts index 146a0a3..8042f73 100644 --- a/src/weatherService.ts +++ b/src/weatherService.ts @@ -25,6 +25,11 @@ const CoordinatesSchema = z.object({ lng: z.number().min(-180).max(180), }); +// Export the GetWeatherOptions interface +export interface GetWeatherOptions { + bypassCache?: boolean; +} + export class WeatherService { private cache: Cache; private providers: IWeatherProvider[]; @@ -65,7 +70,7 @@ export class WeatherService { } // Public method to get weather data for a given latitude and longitude - public async getWeather(lat: number, lng: number) { + public async getWeather(lat: number, lng: number, options?: GetWeatherOptions) { // Validate coordinates const validation = CoordinatesSchema.safeParse({ lat, lng }); if (!validation.success) { @@ -75,45 +80,49 @@ export class WeatherService { // Generate geohash for caching purposes const locationGeohash = geohash.encode(lat, lng, this.geohashPrecision); - // Attempt to retrieve weather data from cache - const cachedWeather = await this.cache.get(locationGeohash); - if (cachedWeather) { - log('Cache hit for geohash:', locationGeohash); - return JSON.parse(cachedWeather); - } else { - log('Cache miss for geohash:', locationGeohash); - let lastError: Error | null = null; - - // Iterate through providers in order of preference - for (const provider of this.providers) { - try { - log(`Trying provider ${provider.name} for (${lat}, ${lng})`); - - // Check if provider supports the given location (e.g., NWS only supports US locations) - if (provider.name === 'nws' && !isLocationInUS(lat, lng)) { - log(`Provider ${provider.name} does not support location (${lat}, ${lng})`); - throw new InvalidProviderLocationError( - `${provider.name} provider does not support the provided location.` - ); - } - - // Attempt to get weather data from the provider - const weather = await provider.getWeather(lat, lng); - - // Store the retrieved weather data in cache - await this.cache.set(locationGeohash, JSON.stringify(weather)); - - // Return the weather data - return weather; - } catch (error) { - log(`Error with provider ${provider.name}:`, error); - lastError = error as Error; - // Continue to the next provider in case of an error - } + let cachedWeather: string | null = null; + + // Attempt to retrieve weather data from cache unless bypassCache is true + if (!options?.bypassCache) { + cachedWeather = await this.cache.get(locationGeohash); + if (cachedWeather) { + log('Cache hit for geohash:', locationGeohash); + return JSON.parse(cachedWeather); } + } + + log('Cache miss or bypassed for geohash:', locationGeohash); + let lastError: Error | null = null; - // If all providers fail, throw the last encountered error - throw lastError || new Error('Unable to retrieve weather data from any provider.'); + // Iterate through providers in order of preference + for (const provider of this.providers) { + try { + log(`Trying provider ${provider.name} for (${lat}, ${lng})`); + + // Check if provider supports the given location (e.g., NWS only supports US locations) + if (provider.name === 'nws' && !isLocationInUS(lat, lng)) { + log(`Provider ${provider.name} does not support location (${lat}, ${lng})`); + throw new InvalidProviderLocationError( + `${provider.name} provider does not support the provided location.` + ); + } + + // Attempt to get weather data from the provider + const weather = await provider.getWeather(lat, lng); + + // Store the retrieved weather data in cache + await this.cache.set(locationGeohash, JSON.stringify(weather)); + + // Return the weather data + return weather; + } catch (error) { + log(`Error with provider ${provider.name}:`, error); + lastError = error as Error; + // Continue to the next provider in case of an error + } } + + // If all providers fail, throw the last encountered error + throw lastError || new Error('Unable to retrieve weather data from any provider.'); } } \ No newline at end of file