Skip to content

Commit

Permalink
Adds support for preventing multiple concurrent fetch calls for data,…
Browse files Browse the repository at this point in the history
… and better caching.
  • Loading branch information
replaysMike committed Apr 3, 2024
1 parent 25be940 commit efa0312
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 29 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
94 changes: 66 additions & 28 deletions src/dataManager.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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;
}
};

Expand All @@ -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;
}
};

Expand All @@ -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;
}
};

};
Expand Down

0 comments on commit efa0312

Please sign in to comment.