MOBILE-3817 core: Support updating WS data in background
parent
f41a4e7b57
commit
89ba05dd3e
|
@ -676,22 +676,59 @@ export class CoreSite {
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
try {
|
try {
|
||||||
let response: T | {exception?: string; errorcode?: string};
|
let response: T | WSCachedError;
|
||||||
|
let cachedData: WSCachedData<T> | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await this.getFromCache<T>(method, data, preSets, false);
|
cachedData = await this.getFromCache<T>(method, data, preSets, false);
|
||||||
|
response = cachedData.response;
|
||||||
} catch {
|
} catch {
|
||||||
// Not found or expired, call WS.
|
// Not found or expired, call WS.
|
||||||
response = await this.getFromWSOrEmergencyCache<T>(method, data, preSets, wsPreSets);
|
response = await this.getFromWS<T>(method, data, preSets, wsPreSets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (('exception' in response && response.exception !== undefined) ||
|
if (
|
||||||
('errorcode' in response && response.errorcode !== undefined)) {
|
typeof response === 'object' && response !== null &&
|
||||||
throw new CoreWSError(response);
|
(
|
||||||
|
('exception' in response && response.exception !== undefined) ||
|
||||||
|
('errorcode' in response && response.errorcode !== undefined)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
subject.error(new CoreWSError(response));
|
||||||
|
} else {
|
||||||
|
subject.next(<T> response);
|
||||||
}
|
}
|
||||||
|
|
||||||
subject.next(<T> response);
|
if (
|
||||||
subject.complete();
|
preSets.updateInBackground &&
|
||||||
|
!CoreConstants.CONFIG.disableCallWSInBackground &&
|
||||||
|
cachedData &&
|
||||||
|
!cachedData.expirationIgnored &&
|
||||||
|
cachedData.expirationTime !== undefined &&
|
||||||
|
Date.now() > cachedData.expirationTime
|
||||||
|
) {
|
||||||
|
// Update the data in background.
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
preSets = {
|
||||||
|
...preSets,
|
||||||
|
emergencyCache: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newData = await this.getFromWS<T>(method, data, preSets, wsPreSets);
|
||||||
|
|
||||||
|
subject.next(newData);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors when updating in background.
|
||||||
|
this.logger.error('Error updating WS data in background', error);
|
||||||
|
} finally {
|
||||||
|
subject.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No need to update in background, complete the observable.
|
||||||
|
subject.complete();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
subject.error(error);
|
subject.error(error);
|
||||||
}
|
}
|
||||||
|
@ -711,7 +748,7 @@ export class CoreSite {
|
||||||
* @param wsPreSets Extra options related to the WS call.
|
* @param wsPreSets Extra options related to the WS call.
|
||||||
* @return Promise resolved with the response.
|
* @return Promise resolved with the response.
|
||||||
*/
|
*/
|
||||||
protected async getFromWSOrEmergencyCache<T = unknown>(
|
protected async getFromWS<T = unknown>(
|
||||||
method: string,
|
method: string,
|
||||||
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
preSets: CoreSiteWSPreSets,
|
preSets: CoreSiteWSPreSets,
|
||||||
|
@ -725,7 +762,7 @@ export class CoreSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.getFromWS<T>(method, data, preSets, wsPreSets);
|
const response = await this.callOrEnqueueWS<T>(method, data, preSets, wsPreSets);
|
||||||
|
|
||||||
if (preSets.saveToCache) {
|
if (preSets.saveToCache) {
|
||||||
this.saveToCache(method, data, response, preSets);
|
this.saveToCache(method, data, response, preSets);
|
||||||
|
@ -820,11 +857,26 @@ export class CoreSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`);
|
this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`);
|
||||||
preSets.omitExpires = true;
|
preSets = {
|
||||||
preSets.getFromCache = true;
|
...preSets,
|
||||||
|
omitExpires: true,
|
||||||
|
getFromCache: true,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.getFromCache<T>(method, data, preSets, true);
|
const cachedData = await this.getFromCache<T>(method, data, preSets, true);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof cachedData.response === 'object' && cachedData.response !== null &&
|
||||||
|
(
|
||||||
|
('exception' in cachedData.response && cachedData.response.exception !== undefined) ||
|
||||||
|
('errorcode' in cachedData.response && cachedData.response.errorcode !== undefined)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new CoreWSError(cachedData.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <T> cachedData.response;
|
||||||
} catch {
|
} catch {
|
||||||
if (useSilentError) {
|
if (useSilentError) {
|
||||||
throw new CoreSilentError(error.message);
|
throw new CoreSilentError(error.message);
|
||||||
|
@ -844,7 +896,7 @@ export class CoreSite {
|
||||||
* @param wsPreSets Extra options related to the WS call.
|
* @param wsPreSets Extra options related to the WS call.
|
||||||
* @return Promise resolved with the response.
|
* @return Promise resolved with the response.
|
||||||
*/
|
*/
|
||||||
protected async getFromWS<T = unknown>(
|
protected async callOrEnqueueWS<T = unknown>(
|
||||||
method: string,
|
method: string,
|
||||||
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
preSets: CoreSiteWSPreSets,
|
preSets: CoreSiteWSPreSets,
|
||||||
|
@ -1085,14 +1137,14 @@ export class CoreSite {
|
||||||
* @param preSets Extra options.
|
* @param preSets Extra options.
|
||||||
* @param emergency Whether it's an "emergency" cache call (WS call failed).
|
* @param emergency Whether it's an "emergency" cache call (WS call failed).
|
||||||
* @param originalData Arguments to pass to the method before being converted to strings.
|
* @param originalData Arguments to pass to the method before being converted to strings.
|
||||||
* @return Promise resolved with the WS response.
|
* @return Cached data.
|
||||||
*/
|
*/
|
||||||
protected async getFromCache<T = unknown>(
|
protected async getFromCache<T = unknown>(
|
||||||
method: string,
|
method: string,
|
||||||
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
preSets: CoreSiteWSPreSets,
|
preSets: CoreSiteWSPreSets,
|
||||||
emergency?: boolean,
|
emergency?: boolean,
|
||||||
): Promise<T> {
|
): Promise<WSCachedData<T>> {
|
||||||
if (!this.db || !preSets.getFromCache) {
|
if (!this.db || !preSets.getFromCache) {
|
||||||
throw new CoreError('Get from cache is disabled.');
|
throw new CoreError('Get from cache is disabled.');
|
||||||
}
|
}
|
||||||
|
@ -1128,12 +1180,22 @@ export class CoreSite {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let expirationTime: number | undefined;
|
let expirationTime: number | undefined;
|
||||||
|
|
||||||
preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !CoreNetwork.isOnline();
|
const forceCache = preSets.omitExpires || preSets.forceOffline || !CoreNetwork.isOnline();
|
||||||
|
|
||||||
if (!preSets.omitExpires) {
|
if (!forceCache) {
|
||||||
expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency);
|
expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency);
|
||||||
|
|
||||||
if (now > expirationTime) {
|
if (preSets.updateInBackground && !CoreConstants.CONFIG.disableCallWSInBackground) {
|
||||||
|
// Use a extended expiration time.
|
||||||
|
const extendedTime = entry.expirationTime +
|
||||||
|
(CoreConstants.CONFIG.callWSInBackgroundExpirationTime ?? CoreConstants.SECONDS_WEEK * 1000);
|
||||||
|
|
||||||
|
if (now > extendedTime) {
|
||||||
|
this.logger.debug('Cached element found, but it is expired even for call WS in background.');
|
||||||
|
|
||||||
|
throw new CoreError('Cache entry is expired.');
|
||||||
|
}
|
||||||
|
} else if (now > expirationTime) {
|
||||||
this.logger.debug('Cached element found, but it is expired');
|
this.logger.debug('Cached element found, but it is expired');
|
||||||
|
|
||||||
throw new CoreError('Cache entry is expired.');
|
throw new CoreError('Cache entry is expired.');
|
||||||
|
@ -1148,7 +1210,11 @@ export class CoreSite {
|
||||||
this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`);
|
this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <T> CoreTextUtils.parseJSON(entry.data, {});
|
return {
|
||||||
|
response: <T> CoreTextUtils.parseJSON(entry.data, {}),
|
||||||
|
expirationIgnored: forceCache,
|
||||||
|
expirationTime,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CoreError('Cache entry not valid.');
|
throw new CoreError('Cache entry not valid.');
|
||||||
|
@ -1591,41 +1657,46 @@ export class CoreSite {
|
||||||
|
|
||||||
this.ongoingRequests[cacheId] = observable;
|
this.ongoingRequests[cacheId] = observable;
|
||||||
|
|
||||||
this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, false).catch(async () => {
|
this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, false)
|
||||||
if (cachePreSets.forceOffline) {
|
.then(cachedData => cachedData.response)
|
||||||
// Don't call the WS, just fail.
|
.catch(async () => {
|
||||||
throw new CoreError(
|
if (cachePreSets.forceOffline) {
|
||||||
Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
|
// Don't call the WS, just fail.
|
||||||
);
|
throw new CoreError(
|
||||||
}
|
Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
|
||||||
|
);
|
||||||
// Call the WS.
|
|
||||||
try {
|
|
||||||
const config = await this.requestPublicConfig();
|
|
||||||
|
|
||||||
if (cachePreSets.saveToCache) {
|
|
||||||
this.saveToCache(method, {}, config, cachePreSets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
// Call the WS.
|
||||||
} catch (error) {
|
|
||||||
cachePreSets.omitExpires = true;
|
|
||||||
cachePreSets.getFromCache = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, true);
|
const config = await this.requestPublicConfig();
|
||||||
} catch {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((response) => {
|
|
||||||
subject.next(response);
|
|
||||||
subject.complete();
|
|
||||||
|
|
||||||
return;
|
if (cachePreSets.saveToCache) {
|
||||||
}).catch((error) => {
|
this.saveToCache(method, {}, config, cachePreSets);
|
||||||
subject.error(error);
|
}
|
||||||
});
|
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
cachePreSets.omitExpires = true;
|
||||||
|
cachePreSets.getFromCache = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cachedData = await this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, true);
|
||||||
|
|
||||||
|
return cachedData.response;
|
||||||
|
} catch {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
// The app doesn't store exceptions for this call, it's safe to assume type CoreSitePublicConfigResponse.
|
||||||
|
subject.next(<CoreSitePublicConfigResponse> response);
|
||||||
|
subject.complete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}).catch((error) => {
|
||||||
|
subject.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
return observable.toPromise();
|
return observable.toPromise();
|
||||||
}
|
}
|
||||||
|
@ -2425,6 +2496,12 @@ export type CoreSiteWSPreSets = {
|
||||||
* can cause the request to fail (see PHP's max_input_vars).
|
* can cause the request to fail (see PHP's max_input_vars).
|
||||||
*/
|
*/
|
||||||
splitRequest?: CoreWSPreSetsSplitRequest;
|
splitRequest?: CoreWSPreSetsSplitRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the app will return cached data even if it's expired and then it'll call the WS in the background.
|
||||||
|
* Only enabled if CoreConstants.CONFIG.disableCallWSInBackground isn't true.
|
||||||
|
*/
|
||||||
|
updateInBackground?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2625,3 +2702,28 @@ export type CoreSiteStoreLastViewedOptions = {
|
||||||
data?: string; // Other data.
|
data?: string; // Other data.
|
||||||
timeaccess?: number; // Accessed time. If not set, current time.
|
timeaccess?: number; // Accessed time. If not set, current time.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info about cached data.
|
||||||
|
*/
|
||||||
|
type WSCachedData<T> = {
|
||||||
|
response: T | WSCachedError; // The WS response data, or an error if the WS returned an error and it was cached.
|
||||||
|
expirationIgnored: boolean; // Whether the expiration time was ignored.
|
||||||
|
expirationTime?: number; // Entry expiration time (only if not ignored).
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error data stored in cache.
|
||||||
|
*/
|
||||||
|
type WSCachedError = {
|
||||||
|
exception?: string;
|
||||||
|
errorcode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable returned when calling WebServices.
|
||||||
|
* If the request uses the "update in background" feature, it will return 2 values: first the cached one, and then the one
|
||||||
|
* coming from the server. After this, it will complete.
|
||||||
|
* Otherwise, it will only return 1 value, either coming from cache or from the server. After this, it will complete.
|
||||||
|
*/
|
||||||
|
export type WSObservable<T> = Observable<T>;
|
||||||
|
|
|
@ -1796,6 +1796,12 @@ export class CoreSitesProvider {
|
||||||
getFromCache: false,
|
getFromCache: false,
|
||||||
emergencyCache: false,
|
emergencyCache: false,
|
||||||
};
|
};
|
||||||
|
case CoreSitesReadingStrategy.UPDATE_IN_BACKGROUND:
|
||||||
|
return {
|
||||||
|
updateInBackground: true,
|
||||||
|
getFromCache: true,
|
||||||
|
saveToCache: true,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -2017,6 +2023,7 @@ export const enum CoreSitesReadingStrategy {
|
||||||
PREFER_CACHE,
|
PREFER_CACHE,
|
||||||
ONLY_NETWORK,
|
ONLY_NETWORK,
|
||||||
PREFER_NETWORK,
|
PREFER_NETWORK,
|
||||||
|
UPDATE_IN_BACKGROUND,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -71,4 +71,6 @@ export interface EnvironmentConfig {
|
||||||
removeaccountonlogout?: boolean; // True to remove the account when the user clicks logout. Doesn't affect switch account.
|
removeaccountonlogout?: boolean; // True to remove the account when the user clicks logout. Doesn't affect switch account.
|
||||||
uselegacycompletion?: boolean; // Whether to use legacy completion by default in all course formats.
|
uselegacycompletion?: boolean; // Whether to use legacy completion by default in all course formats.
|
||||||
toastDurations: Record<ToastDuration, number>;
|
toastDurations: Record<ToastDuration, number>;
|
||||||
|
disableCallWSInBackground?: boolean; // If true, disable calling WS in background.
|
||||||
|
callWSInBackgroundExpirationTime?: number; // Ms to consider an entry expired when calling WS in background. Default: 1 week.
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue