diff --git a/package.json b/package.json index b4ad4cf..85f2e64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-country-state-dropdown", - "version": "1.1.0", + "version": "1.1.1", "description": "A country, state, city, and language dropdown for React", "main": "dist/index.js", "module": "dist/esm/index.js", diff --git a/src/dataManager.js b/src/dataManager.js index 3d36999..a82c210 100644 --- a/src/dataManager.js +++ b/src/dataManager.js @@ -1,8 +1,14 @@ +import _ from 'underscore'; + +/** + * Manages data fetching, caching and controls parallel execution + */ export class DataManager { _log = false; + _operationQueue = []; _countries = []; - _states = {}; - _cities = {}; + _states = []; + _cities = []; _languages = []; constructor() { @@ -14,13 +20,15 @@ export class DataManager { }; async fetchCountries (options) { - if (this._countries.length === 0) { + const cachedData = this._countries; + if (!cachedData || !(cachedData?.length > 0)) { this.log('dataManager: fetching countries', options); - this._countries = await this.getData(options.src, options.files.countries); - return this._countries; + const countries = await this.getData('countries', options.src, options.files.countries); + this._countries = countries; + return countries; } else { this.log('dataManager: cached countries', options); - return this._countries; + return cachedData; } }; @@ -29,13 +37,15 @@ export class DataManager { console.error('fetchStates: error - Country value must be an object.'); return []; } - if (!(country.iso2 in this._states) || this._states[country.iso2].length === 0) { + const cachedData = _.find(this._states, i => i.country === country.iso2); + if (!cachedData || !(cachedData?.states?.length > 0)) { this.log('dataManager: fetching states', country, options.files.states.replace('{country}', country.iso2), options); - this._states[country.iso2] = await this.getData(options.src, options.files.states.replace('{country}', country.iso2)); - return this._states[country.iso2]; + const states = await this.getData(`states-${country.iso2}`, options.src, options.files.states.replace('{country}', country.iso2)); + this._states.push({ country: country.iso2, states }); + return states; } else { this.log('dataManager: cached states', country, options); - return this._states[country.iso2]; + return cachedData.states; } }; @@ -48,37 +58,65 @@ export class DataManager { console.error('fetchCities: error - State value must be an object.'); return []; } - if (!(country.iso2 in this._cities) || !(state.state_code in this._cities[country.iso2]) || this._cities[country.iso2].length === 0) { + const cachedData = _.find(this._cities, i => i.country === country.iso2 && i.state === state.state_code); + if (!cachedData || !(cachedData?.cities?.length > 0)) { this.log('dataManager: fetching cities', country, state, options); - this._cities[country.iso2] = { [state.state_code]: await this.getData(options.src, options.files.cities.replace('{country}', country.iso2).replace('{state}', state.state_code)) }; - return this._cities[country.iso2][state.state_code]; + const cities = await this.getData(`cities-${country.iso2}-${state.state_code}`, options.src, options.files.cities.replace('{country}', country.iso2).replace('{state}', state.state_code)); + this._cities.push({ country: country.iso2, state: state.state_code, cities }); + return cities; } else { this.log('dataManager: cached cities', country, state, options); - return this._cities[country.iso2][state.state_code]; + return cachedData.cities; } }; async fetchLanguages(options) { - if (this._languages.length === 0) { + const cachedData = this._languages; + if (!cachedData || !(cachedData?.length > 0)) { this.log('dataManager: fetching languages', options); - this._languages = await this.getData(options.src, options.files.languages); - return this._languages; + const languages = await this.getData('languages', options.src, options.files.languages); + this._languages = languages; + return languages; } else { this.log('dataManager: cached languages', options); - return this._languages; + return cachedData; } }; - async getData (src, filename) { - const data = await fetch(`${src}${filename}`).then(async (response) => { - if (response.ok) { - const data = await response.json(); - return data; - } else { - console.error(`Failed to fetch geo data file '${filename}'`, response); - } - }); - return data; + /** + * Fetch data from data file in a queue, to prevent concurrent executions for the same resource. + * @param {string} name A unique key for the type of operation being executed + * @param {string} src the base path of the fetch request + * @param {string} filename the name of the data file to fetch + * @returns + */ + async getData (name, src, filename) { + if (!src.endsWith('/')) src += '/'; + + let queueItem = _.find(this._operationQueue, i => i.key === name); + if (queueItem) { + // wait for operation to complete + const data = await queueItem.operation; + return data; + } else { + queueItem = { key: name, operation: null }; + this._operationQueue.push(queueItem); + queueItem.operation = fetch(`${src}${filename}`).then(async (response) => { + if (response.ok) { + const data = await response.json(); + return data; + } else { + console.error(`Failed to fetch geo data file '${filename}'`, response); + return []; + } + }); + + // return the promise + const data = await queueItem.operation; + // remove from queue + this._operationQueue = _.filter(this._operationQueue, i => i.key === name); + return data; + } }; };