auto-retry for some forecast data
This commit is contained in:
		
							parent
							
								
									76fd93e6e1
								
							
						
					
					
						commit
						5dd8f4bd62
					
				
					 16 changed files with 101 additions and 36 deletions
				
			
		| 
						 | 
					@ -19,7 +19,8 @@ class CurrentWeather extends WeatherDisplay {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async getData(_weatherParameters) {
 | 
						async getData(_weatherParameters) {
 | 
				
			||||||
		if (!super.getData(_weatherParameters)) return;
 | 
							// always load the data for use in the lower scroll
 | 
				
			||||||
 | 
							const superResult = super.getData(_weatherParameters);
 | 
				
			||||||
		const weatherParameters = _weatherParameters ?? this.weatherParameters;
 | 
							const weatherParameters = _weatherParameters ?? this.weatherParameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Load the observations
 | 
							// Load the observations
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,8 @@ class CurrentWeather extends WeatherDisplay {
 | 
				
			||||||
					data: {
 | 
										data: {
 | 
				
			||||||
						limit: 2,
 | 
											limit: 2,
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
 | 
										retryCount: 3,
 | 
				
			||||||
 | 
										stillWaiting: () => this.stillWaiting(),
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// test data quality
 | 
									// test data quality
 | 
				
			||||||
| 
						 | 
					@ -60,14 +63,17 @@ class CurrentWeather extends WeatherDisplay {
 | 
				
			||||||
			this.getDataCallback(undefined);
 | 
								this.getDataCallback(undefined);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// preload the icon
 | 
					 | 
				
			||||||
		preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// we only get here if there was no error above
 | 
							// we only get here if there was no error above
 | 
				
			||||||
		this.data = { ...observations, station };
 | 
							this.data = { ...observations, station };
 | 
				
			||||||
		this.setStatus(STATUS.loaded);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.getDataCallback();
 | 
							this.getDataCallback();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stop here if we're disabled
 | 
				
			||||||
 | 
							if (!superResult) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// preload the icon
 | 
				
			||||||
 | 
							preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
 | 
				
			||||||
 | 
							this.setStatus(STATUS.loaded);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// format the data for use outside this function
 | 
						// format the data for use outside this function
 | 
				
			||||||
| 
						 | 
					@ -163,7 +169,8 @@ class CurrentWeather extends WeatherDisplay {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make data available outside this class
 | 
						// make data available outside this class
 | 
				
			||||||
	// promise allows for data to be requested before it is available
 | 
						// promise allows for data to be requested before it is available
 | 
				
			||||||
	async getCurrentWeather() {
 | 
						async getCurrentWeather(stillWaiting) {
 | 
				
			||||||
 | 
							if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
 | 
				
			||||||
		return new Promise((resolve) => {
 | 
							return new Promise((resolve) => {
 | 
				
			||||||
			if (this.data) resolve(this.parseData());
 | 
								if (this.data) resolve(this.parseData());
 | 
				
			||||||
			// data not available, put it into the data callback queue
 | 
								// data not available, put it into the data callback queue
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ const incrementInterval = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const drawScreen = async () => {
 | 
					const drawScreen = async () => {
 | 
				
			||||||
	// get the conditions
 | 
						// get the conditions
 | 
				
			||||||
	const data = await getCurrentWeather();
 | 
						const data = await getCurrentWeather(() => this.stillWaiting());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// nothing to do if there's no data yet
 | 
						// nothing to do if there's no data yet
 | 
				
			||||||
	if (!data) return;
 | 
						if (!data) return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,8 @@ class ExtendedForecast extends WeatherDisplay {
 | 
				
			||||||
				data: {
 | 
									data: {
 | 
				
			||||||
					units: 'us',
 | 
										units: 'us',
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
 | 
									retryCount: 3,
 | 
				
			||||||
 | 
									stillWaiting: () => this.stillWaiting(),
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			console.error('Unable to get extended forecast');
 | 
								console.error('Unable to get extended forecast');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ class HourlyGraph extends WeatherDisplay {
 | 
				
			||||||
	async getData() {
 | 
						async getData() {
 | 
				
			||||||
		if (!super.getData()) return;
 | 
							if (!super.getData()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const data = await getHourlyData();
 | 
							const data = await getHourlyData(() => this.stillWaiting());
 | 
				
			||||||
		if (data === undefined) {
 | 
							if (data === undefined) {
 | 
				
			||||||
			this.setStatus(STATUS.failed);
 | 
								this.setStatus(STATUS.failed);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ class Hourly extends WeatherDisplay {
 | 
				
			||||||
		let forecast;
 | 
							let forecast;
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			// get the forecast
 | 
								// get the forecast
 | 
				
			||||||
			forecast = await json(weatherParameters.forecastGridData);
 | 
								forecast = await json(weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			console.error('Get hourly forecast failed');
 | 
								console.error('Get hourly forecast failed');
 | 
				
			||||||
			console.error(e.status, e.responseJSON);
 | 
								console.error(e.status, e.responseJSON);
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,8 @@ class Hourly extends WeatherDisplay {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make data available outside this class
 | 
						// make data available outside this class
 | 
				
			||||||
	// promise allows for data to be requested before it is available
 | 
						// promise allows for data to be requested before it is available
 | 
				
			||||||
	async getCurrentData() {
 | 
						async getCurrentData(stillWaiting) {
 | 
				
			||||||
 | 
							if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
 | 
				
			||||||
		return new Promise((resolve) => {
 | 
							return new Promise((resolve) => {
 | 
				
			||||||
			if (this.data) resolve(this.data);
 | 
								if (this.data) resolve(this.data);
 | 
				
			||||||
			// data not available, put it into the data callback queue
 | 
								// data not available, put it into the data callback queue
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ class LatestObservations extends WeatherDisplay {
 | 
				
			||||||
		// get data for regional stations
 | 
							// get data for regional stations
 | 
				
			||||||
		const allConditions = await Promise.all(regionalStations.map(async (station) => {
 | 
							const allConditions = await Promise.all(regionalStations.map(async (station) => {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`);
 | 
									const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
 | 
				
			||||||
				// test for temperature, weather and wind values present
 | 
									// test for temperature, weather and wind values present
 | 
				
			||||||
				if (data.properties.temperature.value === null
 | 
									if (data.properties.temperature.value === null
 | 
				
			||||||
					|| data.properties.textDescription === ''
 | 
										|| data.properties.textDescription === ''
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,6 +63,8 @@ class LocalForecast extends WeatherDisplay {
 | 
				
			||||||
				data: {
 | 
									data: {
 | 
				
			||||||
					units: 'us',
 | 
										units: 'us',
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
 | 
									retryCount: 3,
 | 
				
			||||||
 | 
									stillWaiting: () => this.stillWaiting(),
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
 | 
								console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,7 @@ const getWeather = async (latLon) => {
 | 
				
			||||||
const updateStatus = (value) => {
 | 
					const updateStatus = (value) => {
 | 
				
			||||||
	if (value.id < 0) return;
 | 
						if (value.id < 0) return;
 | 
				
			||||||
	if (!progress) return;
 | 
						if (!progress) return;
 | 
				
			||||||
	progress.drawCanvas(displays, countLoadedCanvases());
 | 
						progress.drawCanvas(displays, countLoadedDisplays());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// if this is the first display and we're playing, load it up so it starts playing
 | 
						// if this is the first display and we're playing, load it up so it starts playing
 | 
				
			||||||
	if (isPlaying() && value.id === 0 && value.status === STATUS.loaded) {
 | 
						if (isPlaying() && value.id === 0 && value.status === STATUS.loaded) {
 | 
				
			||||||
| 
						 | 
					@ -106,13 +106,16 @@ const updateStatus = (value) => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// send loaded messaged to parent
 | 
						// send loaded messaged to parent
 | 
				
			||||||
	if (countLoadedCanvases() < displays.length) return;
 | 
						if (countLoadedDisplays() < displays.length) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// everything loaded, set timestamps
 | 
						// everything loaded, set timestamps
 | 
				
			||||||
	AssignLastUpdate(new Date());
 | 
						AssignLastUpdate(new Date());
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const countLoadedCanvases = () => displays.reduce((acc, display) => {
 | 
					// note: a display that is "still waiting"/"retrying" is considered loaded intentionally
 | 
				
			||||||
 | 
					// the weather.gov api has long load times for some products when you are the first
 | 
				
			||||||
 | 
					// requester for the product after the cache expires
 | 
				
			||||||
 | 
					const countLoadedDisplays = () => displays.reduce((acc, display) => {
 | 
				
			||||||
	if (display.status !== STATUS.loading) return acc + 1;
 | 
						if (display.status !== STATUS.loading) return acc + 1;
 | 
				
			||||||
	return acc;
 | 
						return acc;
 | 
				
			||||||
}, 0);
 | 
					}, 0);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,6 +55,9 @@ class Progress extends WeatherDisplay {
 | 
				
			||||||
			case STATUS.disabled:
 | 
								case STATUS.disabled:
 | 
				
			||||||
				statusClass = 'disabled';
 | 
									statusClass = 'disabled';
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
 | 
								case STATUS.retrying:
 | 
				
			||||||
 | 
									statusClass = 'retrying';
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
			default:
 | 
								default:
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ const STATUS = {
 | 
				
			||||||
	failed: Symbol('failed'),
 | 
						failed: Symbol('failed'),
 | 
				
			||||||
	noData: Symbol('noData'),
 | 
						noData: Symbol('noData'),
 | 
				
			||||||
	disabled: Symbol('disabled'),
 | 
						disabled: Symbol('disabled'),
 | 
				
			||||||
 | 
						retrying: Symbol('retyring'),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default STATUS;
 | 
					export default STATUS;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,8 +11,12 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
 | 
				
			||||||
		method: 'GET',
 | 
							method: 'GET',
 | 
				
			||||||
		mode: 'cors',
 | 
							mode: 'cors',
 | 
				
			||||||
		type: 'GET',
 | 
							type: 'GET',
 | 
				
			||||||
 | 
							retryCount: 0,
 | 
				
			||||||
		..._params,
 | 
							..._params,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
						// store original number of retries
 | 
				
			||||||
 | 
						params.originalRetries = params.retryCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// build a url, including the rewrite for cors if necessary
 | 
						// build a url, including the rewrite for cors if necessary
 | 
				
			||||||
	let corsUrl = _url;
 | 
						let corsUrl = _url;
 | 
				
			||||||
	if (params.cors === true) corsUrl = rewriteUrl(_url);
 | 
						if (params.cors === true) corsUrl = rewriteUrl(_url);
 | 
				
			||||||
| 
						 | 
					@ -30,7 +34,7 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make the request
 | 
						// make the request
 | 
				
			||||||
	const response = await fetch(url, params);
 | 
						const response = await doFetch(url, params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check for ok response
 | 
						// check for ok response
 | 
				
			||||||
	if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
 | 
						if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
 | 
				
			||||||
| 
						 | 
					@ -47,6 +51,48 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fetch with retry and back-off
 | 
				
			||||||
 | 
					const doFetch = (url, params) => new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
						fetch(url, params).then((response) => {
 | 
				
			||||||
 | 
							if (params.retryCount > 0) {
 | 
				
			||||||
 | 
								// 500 status codes should be retried after a short backoff
 | 
				
			||||||
 | 
								if (response.status >= 500 && response.status <= 599 && params.retryCount > 0) {
 | 
				
			||||||
 | 
									// call the "still waiting" function
 | 
				
			||||||
 | 
									if (typeof params.stillWaiting === 'function' && params.retryCount === params.originalRetries) {
 | 
				
			||||||
 | 
										params.stillWaiting();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// decrement and retry
 | 
				
			||||||
 | 
									const newParams = {
 | 
				
			||||||
 | 
										...params,
 | 
				
			||||||
 | 
										retryCount: params.retryCount - 1,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
									return resolve(delay(retryDelay(params.originalRetries - newParams.retryCount), doFetch, url, newParams));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// not 500 status
 | 
				
			||||||
 | 
								return resolve(response);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// out of retries
 | 
				
			||||||
 | 
							return resolve(response);
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
							.catch((e) => reject(e));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const delay = (time, func, ...args) => new Promise((resolve) => {
 | 
				
			||||||
 | 
						setTimeout(() => {
 | 
				
			||||||
 | 
							resolve(func(...args));
 | 
				
			||||||
 | 
						}, time);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const retryDelay = (retryNumber) => {
 | 
				
			||||||
 | 
						switch (retryNumber) {
 | 
				
			||||||
 | 
						case 1: return 1000;
 | 
				
			||||||
 | 
						case 2: return 2000;
 | 
				
			||||||
 | 
						case 3: return 5000;
 | 
				
			||||||
 | 
						case 4: return 10000;
 | 
				
			||||||
 | 
						default: return 30000;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
	json,
 | 
						json,
 | 
				
			||||||
	text,
 | 
						text,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ class WeatherDisplay {
 | 
				
			||||||
		this.loadingStatus = STATUS.loading;
 | 
							this.loadingStatus = STATUS.loading;
 | 
				
			||||||
		this.name = name ?? elemId;
 | 
							this.name = name ?? elemId;
 | 
				
			||||||
		this.getDataCallbacks = [];
 | 
							this.getDataCallbacks = [];
 | 
				
			||||||
 | 
							this.stillWaitingCallbacks = [];
 | 
				
			||||||
		this.defaultEnabled = defaultEnabled;
 | 
							this.defaultEnabled = defaultEnabled;
 | 
				
			||||||
		this.okToDrawCurrentConditions = true;
 | 
							this.okToDrawCurrentConditions = true;
 | 
				
			||||||
		this.okToDrawCurrentDateTime = true;
 | 
							this.okToDrawCurrentDateTime = true;
 | 
				
			||||||
| 
						 | 
					@ -392,6 +393,14 @@ class WeatherDisplay {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return template;
 | 
							return template;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// still waiting for data (retries triggered)
 | 
				
			||||||
 | 
						stillWaiting() {
 | 
				
			||||||
 | 
							if (this.enabled) this.setStatus(STATUS.retrying);
 | 
				
			||||||
 | 
							// handle still waiting callbacks
 | 
				
			||||||
 | 
							this.stillWaitingCallbacks.forEach((callback) => callback());
 | 
				
			||||||
 | 
							this.stillWaitingCallbacks = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default WeatherDisplay;
 | 
					export default WeatherDisplay;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
					@ -37,7 +37,8 @@
 | 
				
			||||||
					padding-left: 4px;
 | 
										padding-left: 4px;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				.loading {
 | 
									.loading,
 | 
				
			||||||
 | 
									.retrying {
 | 
				
			||||||
					color: #ffff00;
 | 
										color: #ffff00;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,23 +59,12 @@
 | 
				
			||||||
					color: #C0C0C0;
 | 
										color: #C0C0C0;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&.loading .loading {
 | 
									&.loading .loading,
 | 
				
			||||||
					display: block;
 | 
									&.press-here .press-here,
 | 
				
			||||||
				}
 | 
									&.failed .failed,
 | 
				
			||||||
 | 
									&.no-data .no-data,
 | 
				
			||||||
				&.press-here .press-here {
 | 
									&.disabled .disabled,
 | 
				
			||||||
					display: block;
 | 
									&.retrying .retrying {
 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				&.failed .failed {
 | 
					 | 
				
			||||||
					display: block;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				&.no-data .no-data {
 | 
					 | 
				
			||||||
					display: block;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				&.disabled .disabled {
 | 
					 | 
				
			||||||
					display: block;
 | 
										display: block;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@
 | 
				
			||||||
        <div class="failed">Failed</div>
 | 
					        <div class="failed">Failed</div>
 | 
				
			||||||
        <div class="no-data">No Data</div>
 | 
					        <div class="no-data">No Data</div>
 | 
				
			||||||
        <div class="disabled">Disabled</div>
 | 
					        <div class="disabled">Disabled</div>
 | 
				
			||||||
 | 
					        <div class="retrying">Retrying</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue