Skip to content

Commit

Permalink
Add the ability to bypass cache for a request
Browse files Browse the repository at this point in the history
  • Loading branch information
victorquinn committed Oct 10, 2024
1 parent e171092 commit 8acfbed
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 44 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
40 changes: 39 additions & 1 deletion src/weatherService.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 });
});
});
85 changes: 47 additions & 38 deletions src/weatherService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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) {
Expand All @@ -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.');
}
}

0 comments on commit 8acfbed

Please sign in to comment.