Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copilot Review Standards

- Focus on correctness, regressions, security, performance, and missing tests.
- Skip pure style/naming/comment nits unless they affect behavior or maintainability.
- Check previous PR comments to make sure you're not being overly pedantic.
5 changes: 5 additions & 0 deletions src/pkjs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ function fetch(provider, force) {
return;
}

if (typeof provider.isGeocodeBackoffActive === 'function' && provider.isGeocodeBackoffActive()) {
console.log('Skipping weather fetch: geocoding is in backoff cooldown.');
return;
}

console.log('Fetching from ' + provider.name);
var fetchStart = Date.now();
var attempt = incrementFetchAttemptCounter();
Expand Down
161 changes: 119 additions & 42 deletions src/pkjs/weather/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ function readStoredJson(key) {
}
}

/**
* Normalize a location query for cache lookups.
*
* @param {string} location Query string.
* @returns {string} Normalized query string.
*/
function normalizeLocationQuery(location) {
return location.trim();
}

/**
* Read the cached geocode result for the active location.
*
Expand All @@ -89,9 +99,18 @@ function readStoredJson(key) {
*/
function readGeocodeCache(location) {
var cachedGeocode = readStoredJson(GEOCODE_CACHE_KEY);
var normalizedLocation = normalizeLocationQuery(location);
var cachedQuery;

if (cachedGeocode && typeof cachedGeocode.query === 'string') {
cachedQuery = normalizeLocationQuery(cachedGeocode.query);
if (cachedQuery === normalizedLocation) {
return cachedGeocode;
}
}

if (cachedGeocode && cachedGeocode.query === location) {
return cachedGeocode;
if (cachedGeocode && typeof cachedGeocode.query !== 'string') {
localStorage.removeItem(GEOCODE_CACHE_KEY);
}

return null;
Expand All @@ -107,33 +126,13 @@ function readGeocodeCache(location) {
*/
function writeGeocodeCache(location, lat, lon) {
localStorage.setItem(GEOCODE_CACHE_KEY, JSON.stringify({
query: location,
query: normalizeLocationQuery(location),
lat: lat,
lon: lon,
time: Date.now()
}));
}

/**
* Determine whether geocoding is still in backoff.
*
* @returns {boolean} True when geocoding should be skipped.
*/
function isGeocodeBackoffActive() {
var backoffData = readStoredJson(RATE_LIMIT_BACKOFF_KEY);

if (!backoffData) {
return false;
}

if (Date.now() < (backoffData.until || 0)) {
return true;
}

localStorage.removeItem(RATE_LIMIT_BACKOFF_KEY);
return false;
}

/**
* Record a LocationIQ 429 backoff window.
*
Expand Down Expand Up @@ -173,6 +172,36 @@ WeatherProvider.prototype.gpsOverride = function(location) {
this.location = location;
};

/**
* Determine whether the provider is currently rate-limited for geocoding.
*
* @returns {boolean} True when forward geocoding should be skipped.
*/
WeatherProvider.prototype.isGeocodeBackoffActive = function() {
Comment thread
mattrossman marked this conversation as resolved.
var locationOverride = parseLocationOverride(this.location);
var backoffData;

if (locationOverride.type !== 'manual_address') {
return false;
}

if (readGeocodeCache(locationOverride.query) !== null) {
return false;
}

backoffData = readStoredJson(RATE_LIMIT_BACKOFF_KEY);
if (!backoffData) {
return false;
}

if (Date.now() < (backoffData.until || 0)) {
return true;
}

localStorage.removeItem(RATE_LIMIT_BACKOFF_KEY);
return false;
};
Comment thread
mattrossman marked this conversation as resolved.

WeatherProvider.prototype.withSunEvents = function(lat, lon, callback, onFailure) {
/* The callback runs with an array of the next two sun events (i.e. 24 hours worth),
* where each sun event contains a 'type' ('sunrise' or 'sunset') and a 'date' (of type Date)
Expand Down Expand Up @@ -256,46 +285,91 @@ WeatherProvider.prototype.withCityName = function(lat, lon, callback, onFailure)
// https://github.com/mattrossman/forecaswatch2/issues/59#issue-1317582743
var r_lat_long = /^([-+]?\d*\.?\d+)\s*,\s*([-+]?\d*\.?\d+)$/;

/**
* Parse a location override into GPS, manual coordinates, or an address.
*
* @param {*} location Location override value.
* @returns {{ type: 'gps'|'manual_coordinates'|'manual_address', query: string|null, latitude: string|null, longitude: string|null }} Parsed override state.
*/
function parseLocationOverride(location) {
var trimmedLocation;
var match;

trimmedLocation = typeof location === 'string' ? normalizeLocationQuery(location) : null;
if (trimmedLocation === null || trimmedLocation.length === 0) {
return {
type: 'gps',
query: null,
latitude: null,
longitude: null
};
}

match = trimmedLocation.match(r_lat_long);
if (match !== null) {
return {
type: 'manual_coordinates',
query: trimmedLocation,
latitude: match[1],
longitude: match[2]
};
}

return {
type: 'manual_address',
query: trimmedLocation,
latitude: null,
longitude: null
};
}

WeatherProvider.prototype.withGeocodeCoordinates = function(callback, onFailure) {
// callback(latitude, longitude)
var locationiqKey = 'pk.5a61972cde94491774bcfaa0705d5a0d';
var url = 'https://us1.locationiq.com/v1/search.php?key=' + locationiqKey
+ '&q=' + encodeURIComponent(this.location)
+ '&format=json';
var m = this.location.match(r_lat_long);
var locationOverride = parseLocationOverride(this.location);
var url;
var latitude;
var longitude;
var cachedGeocode;
var backoffMs;

console.log('WeatherProvider.prototype.withGeocodeCoordinates regex, this.location: ' + JSON.stringify(this.location));
if (m !== null) {
latitude = m[1];
longitude = m[2];
console.log('WeatherProvider.prototype.withGeocodeCoordinates override: ' + JSON.stringify(this.location));
if (locationOverride.type === 'manual_coordinates') {
latitude = locationOverride.latitude;
longitude = locationOverride.longitude;
this.locationMode = 'manual_coordinates';
console.log('regex matched, override is lat/long');
callback(latitude, longitude);
return;
}

Comment thread
mattrossman marked this conversation as resolved.
// Check rate limit backoff: skip geocoding if we're still in cooldown from a 429
if (isGeocodeBackoffActive()) {
console.log('[!] Geocoding in backoff cooldown, skipping');
onFailure(failure('forward_geocode', 'backoff'));
if (locationOverride.type !== 'manual_address') {
onFailure(failure('forward_geocode', 'invalid_location'));
return;
}

// Check geocode cache: if the same address string was resolved before, reuse it
cachedGeocode = readGeocodeCache(this.location);
url = 'https://us1.locationiq.com/v1/search.php?key=' + locationiqKey
+ '&q=' + encodeURIComponent(locationOverride.query)
+ '&format=json';

// Keep cached coordinates usable even while LocationIQ is in backoff.
cachedGeocode = readGeocodeCache(locationOverride.query);
if (cachedGeocode !== null) {
console.log('Using cached geocode for: ' + this.location);
console.log('Using cached geocode for: ' + locationOverride.query);
this.locationMode = 'manual_address';
callback(cachedGeocode.lat, cachedGeocode.lon);
return;
}

// Check rate limit backoff: skip geocoding if we're still in cooldown from a 429
if (this.isGeocodeBackoffActive()) {
console.log('[!] Geocoding in backoff cooldown, skipping');
onFailure(failure('forward_geocode', 'backoff'));
return;
}

this.locationMode = 'manual_address';
console.log('regex failed, about to look up lat/long for override');
console.log('Looking up coordinates for address override');
request(
url,
'GET',
Expand All @@ -317,9 +391,9 @@ WeatherProvider.prototype.withGeocodeCoordinates = function(callback, onFailure)
}

closest = locations[0];
console.log('Query ' + this.location + ' geocoded to ' + closest.lat + ', ' + closest.lon);
console.log('Query ' + locationOverride.query + ' geocoded to ' + closest.lat + ', ' + closest.lon);
// Cache the successful geocode result
writeGeocodeCache(this.location, closest.lat, closest.lon);
writeGeocodeCache(locationOverride.query, closest.lat, closest.lon);
callback(closest.lat, closest.lon);
}).bind(this),
(function(error) {
Expand Down Expand Up @@ -414,11 +488,14 @@ WeatherProvider.prototype.withGpsCoordinates = function(callback, onFailure) {
};

WeatherProvider.prototype.withCoordinates = function(callback, onFailure) {
var locationOverride;

this.usedGpsCache = false;
this.gpsErrorCode = null;
this.locationMode = null;

if (this.location === null) {
locationOverride = parseLocationOverride(this.location);
if (locationOverride.type === 'gps') {
this.locationMode = 'gps';
console.log('Using GPS');
this.withGpsCoordinates(callback, onFailure);
Expand Down