Gracefully handle backend failures in Expo app
This commit is contained in:
119
API.js
119
API.js
@@ -1,6 +1,38 @@
|
||||
|
||||
const baseUrl = "https://emiapi.reynafamily.com";
|
||||
//const baseUrl = "http://localhost:3000";
|
||||
const requestErrorCooldownMs = 30000;
|
||||
const profileFailureCooldownMs = 60000;
|
||||
const recentRequestErrors = {};
|
||||
const failedProfileCache = {};
|
||||
|
||||
const normalizePath = (path = "") => {
|
||||
return path.replace(/[a-f0-9]{24}/gi, ":id");
|
||||
};
|
||||
|
||||
const logRequestErrorOnce = (key, message) => {
|
||||
const now = Date.now();
|
||||
if (recentRequestErrors[key] && now - recentRequestErrors[key] < requestErrorCooldownMs) return;
|
||||
recentRequestErrors[key] = now;
|
||||
console.error(message);
|
||||
};
|
||||
|
||||
const getFallbackForPath = (path = "") => {
|
||||
if (path.startsWith("/post/") || path === "/post/") return [];
|
||||
return {};
|
||||
};
|
||||
|
||||
const parseJsonResponse = async (response, path = "", fallback) => {
|
||||
const text = await response.text();
|
||||
if (!text) return fallback;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
const normalizedPath = normalizePath(path);
|
||||
logRequestErrorOnce(`invalid-json:${normalizedPath}`, `Invalid JSON from ${normalizedPath}: ${text.slice(0, 200)}`);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
let getCall = async (path = "", params = {}) => {
|
||||
let queryParams = "?";
|
||||
@@ -8,6 +40,7 @@ let getCall = async (path = "", params = {}) => {
|
||||
queryParams += p + "=" + params[p] + "&"
|
||||
});
|
||||
let localBaseUrl = global.baseUrl ?? baseUrl;
|
||||
const fallback = getFallbackForPath(path);
|
||||
return fetch(localBaseUrl + path + queryParams, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
@@ -15,9 +48,17 @@ let getCall = async (path = "", params = {}) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}).then(response => response.json()).catch((error) => {
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}).then(async (response) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
if (!response.ok) {
|
||||
logRequestErrorOnce(`GET:${normalizedPath}:${response.status}`, `GET ${normalizedPath} failed: ${response.status}`);
|
||||
return fallback;
|
||||
}
|
||||
return parseJsonResponse(response, path, fallback);
|
||||
}).catch((error) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
logRequestErrorOnce(`GET:${normalizedPath}:network`, `GET ${normalizedPath} network error: ${error?.message || error}`);
|
||||
return fallback;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,6 +68,7 @@ let deleteCall = async (path = "", params = {}) => {
|
||||
queryParams += p + "=" + params[p] + "&"
|
||||
});
|
||||
let localBaseUrl = global.baseUrl ?? baseUrl;
|
||||
const fallback = {};
|
||||
return fetch(localBaseUrl + path + queryParams, {
|
||||
method: 'DELETE',
|
||||
mode: 'cors',
|
||||
@@ -34,14 +76,23 @@ let deleteCall = async (path = "", params = {}) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}).then(response => response.json()).catch((error) => {
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}).then(async (response) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
if (!response.ok) {
|
||||
logRequestErrorOnce(`DELETE:${normalizedPath}:${response.status}`, `DELETE ${normalizedPath} failed: ${response.status}`);
|
||||
return fallback;
|
||||
}
|
||||
return parseJsonResponse(response, path, fallback);
|
||||
}).catch((error) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
logRequestErrorOnce(`DELETE:${normalizedPath}:network`, `DELETE ${normalizedPath} network error: ${error?.message || error}`);
|
||||
return fallback;
|
||||
})
|
||||
}
|
||||
|
||||
let postCall = async (path, params) => {
|
||||
let localBaseUrl = global.baseUrl ?? baseUrl;
|
||||
const fallback = {};
|
||||
return fetch(localBaseUrl + path, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
@@ -50,9 +101,17 @@ let postCall = async (path, params) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}).then(response => response.json()).catch((error) => {
|
||||
console.error(error);
|
||||
console.trace();
|
||||
}).then(async (response) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
if (!response.ok) {
|
||||
logRequestErrorOnce(`POST:${normalizedPath}:${response.status}`, `POST ${normalizedPath} failed: ${response.status}`);
|
||||
return fallback;
|
||||
}
|
||||
return parseJsonResponse(response, path, fallback);
|
||||
}).catch((error) => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
logRequestErrorOnce(`POST:${normalizedPath}:network`, `POST ${normalizedPath} network error: ${error?.message || error}`);
|
||||
return fallback;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,14 +120,44 @@ let CurrentProfile;
|
||||
let CurrentProfileData;
|
||||
let userNameCache = {}; //save this on localstorage
|
||||
let working_on = {}; //Promises
|
||||
const emptyProfileData = (id = "") => ({
|
||||
_id: id || "",
|
||||
profile: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
photo: "",
|
||||
description: "",
|
||||
language: "en",
|
||||
},
|
||||
data: {},
|
||||
following: [],
|
||||
});
|
||||
let getProfileFromCache = async (id, refresh=false) => {
|
||||
if(!id) console.trace();
|
||||
const lastFailure = failedProfileCache[id] || 0;
|
||||
if (!refresh && lastFailure && Date.now() - lastFailure < profileFailureCooldownMs) {
|
||||
return emptyProfileData(id);
|
||||
}
|
||||
if (userNameCache[id] && !refresh)
|
||||
return userNameCache[id];
|
||||
if (working_on[id] && !refresh)
|
||||
return working_on[id];
|
||||
//console.log(id, "not in cache, getting...")
|
||||
working_on[id] = getCall("/user/" + id)
|
||||
working_on[id] = getCall("/user/" + id).then((profile) => {
|
||||
const hasRealProfile = !!(profile && profile._id);
|
||||
const safeProfile = hasRealProfile ? { ...emptyProfileData(id), ...profile, profile: { ...emptyProfileData(id).profile, ...(profile.profile || {}) } } : emptyProfileData(id);
|
||||
if (hasRealProfile) {
|
||||
userNameCache[id] = safeProfile;
|
||||
delete failedProfileCache[id];
|
||||
} else {
|
||||
failedProfileCache[id] = Date.now();
|
||||
}
|
||||
delete working_on[id];
|
||||
return safeProfile;
|
||||
}).catch(() => {
|
||||
failedProfileCache[id] = Date.now();
|
||||
delete working_on[id];
|
||||
return emptyProfileData(id);
|
||||
});
|
||||
return working_on[id];
|
||||
}
|
||||
|
||||
@@ -140,8 +229,8 @@ const API = {
|
||||
},
|
||||
//Posts
|
||||
getPosts(userid) {
|
||||
if (userid) return getCall("/post/usr/" + userid);
|
||||
return getCall("/post/");
|
||||
if (userid) return getCall("/post/usr/" + userid).then((data) => Array.isArray(data) ? data : []);
|
||||
return getCall("/post/").then((data) => Array.isArray(data) ? data : []);
|
||||
},
|
||||
getPostsByTag(tag) {
|
||||
return getCall("/post/tag/" + tag);
|
||||
@@ -235,7 +324,7 @@ const API = {
|
||||
});
|
||||
},
|
||||
getUserProfile(profileid, refresh=false) {
|
||||
return getProfileFromCache(profileid, refresh);
|
||||
return getProfileFromCache(profileid, refresh).then((profile) => profile || emptyProfileData(profileid));
|
||||
},
|
||||
deleteProfile(profileid){
|
||||
return deleteCall("/user/" + profileid);
|
||||
@@ -325,4 +414,4 @@ const API = {
|
||||
}
|
||||
}
|
||||
|
||||
export default API;
|
||||
export default API;
|
||||
|
||||
Reference in New Issue
Block a user