MOBILE-3977 core: Implement async instance pattern

main
Noel De Martin 2022-02-03 13:56:36 +01:00
parent a041471205
commit 7a2a8c3e98
4 changed files with 158 additions and 68 deletions

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CorePromisedValue } from '@classes/promised-value'; import { asyncInstance } from '@/core/utils/async-instance';
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreConfigProvider } from '@services/config'; import { CoreConfigProvider } from '@services/config';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
@ -34,7 +34,7 @@ export class CoreDatabaseTableProxy<
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> { > extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
protected config: CoreDatabaseConfiguration; protected config: CoreDatabaseConfiguration;
protected target: CorePromisedValue<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> = new CorePromisedValue(); protected target = asyncInstance<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>();
protected environmentObserver?: CoreEventObserver; protected environmentObserver?: CoreEventObserver;
constructor( constructor(
@ -68,81 +68,63 @@ export class CoreDatabaseTableProxy<
* @inheritdoc * @inheritdoc
*/ */
async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> { async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
const target = await this.target; return this.target.all(conditions);
return target.all(conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async find(conditions: Partial<DBRecord>): Promise<DBRecord> { async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
const target = await this.target; return this.target.find(conditions);
return target.find(conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> { async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
const target = await this.target; return this.target.findByPrimaryKey(primaryKey);
return target.findByPrimaryKey(primaryKey);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> { async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
const target = await this.target; return this.target.reduce<T>(reducer, conditions);
return target.reduce<T>(reducer, conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async insert(record: DBRecord): Promise<void> { async insert(record: DBRecord): Promise<void> {
const target = await this.target; return this.target.insert(record);
return target.insert(record);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> { async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
const target = await this.target; return this.target.update(updates, conditions);
return target.update(updates, conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> { async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
const target = await this.target; return this.target.updateWhere(updates, conditions);
return target.updateWhere(updates, conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async delete(conditions?: Partial<DBRecord>): Promise<void> { async delete(conditions?: Partial<DBRecord>): Promise<void> {
const target = await this.target; return this.target.delete(conditions);
return target.delete(conditions);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> { async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
const target = await this.target; return this.target.deleteByPrimaryKey(primaryKey);
return target.deleteByPrimaryKey(primaryKey);
} }
/** /**
@ -174,18 +156,18 @@ export class CoreDatabaseTableProxy<
* Update underlying target instance. * Update underlying target instance.
*/ */
protected async updateTarget(): Promise<void> { protected async updateTarget(): Promise<void> {
const oldTarget = this.target.value; const oldTarget = this.target.instance;
const newTarget = this.createTarget(); const newTarget = this.createTarget();
if (oldTarget) { if (oldTarget) {
await oldTarget.destroy(); await oldTarget.destroy();
this.target.reset(); this.target.resetInstance();
} }
await newTarget.initialize(); await newTarget.initialize();
this.target.resolve(newTarget); this.target.setInstance(newTarget);
} }
/** /**

View File

@ -42,6 +42,8 @@ import { Translate } from '@singletons';
import { CoreIonLoadingElement } from './ion-loading'; import { CoreIonLoadingElement } from './ion-loading';
import { CoreLang } from '@services/lang'; import { CoreLang } from '@services/lang';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CoreDatabaseTable } from './database/database-table';
/** /**
* QR Code type enumeration. * QR Code type enumeration.
@ -104,6 +106,7 @@ export class CoreSite {
// Rest of variables. // Rest of variables.
protected logger: CoreLogger; protected logger: CoreLogger;
protected db?: SQLiteDB; protected db?: SQLiteDB;
protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
protected cleanUnicode = false; protected cleanUnicode = false;
protected lastAutoLogin = 0; protected lastAutoLogin = 0;
protected offlineDisabled = false; protected offlineDisabled = false;
@ -137,6 +140,7 @@ export class CoreSite {
) { ) {
this.logger = CoreLogger.getInstance('CoreSite'); this.logger = CoreLogger.getInstance('CoreSite');
this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params. this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
this.cacheTable = asyncInstance(() => CoreSites.getCacheTable(this));
this.setInfo(infos); this.setInfo(infos);
this.calculateOfflineDisabled(); this.calculateOfflineDisabled();
@ -926,15 +930,14 @@ export class CoreSite {
} }
const id = this.getCacheId(method, data); const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
let entry: CoreSiteWSCacheRecord | undefined; let entry: CoreSiteWSCacheRecord | undefined;
if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) {
const entries = await cacheTable.all({ key: preSets.cacheKey }); const entries = await this.cacheTable.all({ key: preSets.cacheKey });
if (!entries.length) { if (!entries.length) {
// Cache key not found, get by params sent. // Cache key not found, get by params sent.
entry = await cacheTable.findByPrimaryKey({ id }); entry = await this.cacheTable.findByPrimaryKey({ id });
} else { } else {
if (entries.length > 1) { if (entries.length > 1) {
// More than one entry found. Search the one with same ID as this call. // More than one entry found. Search the one with same ID as this call.
@ -946,7 +949,7 @@ export class CoreSite {
} }
} }
} else { } else {
entry = await cacheTable.findByPrimaryKey({ id }); entry = await this.cacheTable.findByPrimaryKey({ id });
} }
if (entry === undefined) { if (entry === undefined) {
@ -991,14 +994,13 @@ export class CoreSite {
*/ */
async getComponentCacheSize(component: string, componentId?: number): Promise<number> { async getComponentCacheSize(component: string, componentId?: number): Promise<number> {
const params: Array<string | number> = [component]; const params: Array<string | number> = [component];
const cacheTable = await CoreSites.getCacheTable(this);
let extraClause = ''; let extraClause = '';
if (componentId !== undefined && componentId !== null) { if (componentId !== undefined && componentId !== null) {
params.push(componentId); params.push(componentId);
extraClause = ' AND componentId = ?'; extraClause = ' AND componentId = ?';
} }
return cacheTable.reduce( return this.cacheTable.reduce(
{ {
sql: 'SUM(length(data))', sql: 'SUM(length(data))',
js: (size, record) => size + record.data.length, js: (size, record) => size + record.data.length,
@ -1031,7 +1033,6 @@ export class CoreSite {
// Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time. // Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time.
// We decided to reuse this field to prevent modifying the database table. // We decided to reuse this field to prevent modifying the database table.
const id = this.getCacheId(method, data); const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
const entry = { const entry = {
id, id,
data: JSON.stringify(response), data: JSON.stringify(response),
@ -1049,7 +1050,7 @@ export class CoreSite {
} }
} }
await cacheTable.insert(entry); await this.cacheTable.insert(entry);
} }
/** /**
@ -1064,12 +1065,11 @@ export class CoreSite {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> { protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> {
const id = this.getCacheId(method, data); const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
if (allCacheKey) { if (allCacheKey) {
await cacheTable.delete({ key: preSets.cacheKey }); await this.cacheTable.delete({ key: preSets.cacheKey });
} else { } else {
await cacheTable.deleteByPrimaryKey({ id }); await this.cacheTable.deleteByPrimaryKey({ id });
} }
} }
@ -1087,13 +1087,12 @@ export class CoreSite {
} }
const params = { component }; const params = { component };
const cacheTable = await CoreSites.getCacheTable(this);
if (componentId) { if (componentId) {
params['componentId'] = componentId; params['componentId'] = componentId;
} }
await cacheTable.delete(params); await this.cacheTable.delete(params);
} }
/* /*
@ -1128,9 +1127,7 @@ export class CoreSite {
this.logger.debug('Invalidate all the cache for site: ' + this.id); this.logger.debug('Invalidate all the cache for site: ' + this.id);
try { try {
const cacheTable = await CoreSites.getCacheTable(this); await this.cacheTable.update({ expirationTime: 0 });
await cacheTable.update({ expirationTime: 0 });
} finally { } finally {
CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId()); CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
} }
@ -1149,9 +1146,7 @@ export class CoreSite {
this.logger.debug('Invalidate cache for key: ' + key); this.logger.debug('Invalidate cache for key: ' + key);
const cacheTable = await CoreSites.getCacheTable(this); await this.cacheTable.update({ expirationTime: 0 }, { key });
await cacheTable.update({ expirationTime: 0 }, { key });
} }
/** /**
@ -1185,9 +1180,7 @@ export class CoreSite {
this.logger.debug('Invalidate cache for key starting with: ' + key); this.logger.debug('Invalidate cache for key starting with: ' + key);
const cacheTable = await CoreSites.getCacheTable(this); await this.cacheTable.updateWhere({ expirationTime: 0 }, {
await cacheTable.updateWhere({ expirationTime: 0 }, {
sql: 'key LIKE ?', sql: 'key LIKE ?',
sqlParams: [key], sqlParams: [key],
js: record => !!record.key?.startsWith(key), js: record => !!record.key?.startsWith(key),
@ -1266,9 +1259,7 @@ export class CoreSite {
* @return Promise resolved with the total size of all data in the cache table (bytes) * @return Promise resolved with the total size of all data in the cache table (bytes)
*/ */
async getCacheUsage(): Promise<number> { async getCacheUsage(): Promise<number> {
const cacheTable = await CoreSites.getCacheTable(this); return this.cacheTable.reduce({
return cacheTable.reduce({
sql: 'SUM(length(data))', sql: 'SUM(length(data))',
js: (size, record) => size + record.data.length, js: (size, record) => size + record.data.length,
jsInitialValue: 0, jsInitialValue: 0,

View File

@ -21,7 +21,7 @@ import { makeSingleton } from '@singletons';
import { CoreConstants } from '../constants'; import { CoreConstants } from '../constants';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseTable } from '@classes/database/database-table';
import { CorePromisedValue } from '@classes/promised-value'; import { asyncInstance } from '../utils/async-instance';
declare module '@singletons/events' { declare module '@singletons/events' {
@ -45,7 +45,7 @@ export class CoreConfigProvider {
static readonly ENVIRONMENT_UPDATED = 'environment_updated'; static readonly ENVIRONMENT_UPDATED = 'environment_updated';
protected table: CorePromisedValue<CoreDatabaseTable<ConfigDBEntry, 'name'>> = new CorePromisedValue(); protected table = asyncInstance<CoreDatabaseTable<ConfigDBEntry, 'name'>>();
protected defaultEnvironment?: EnvironmentConfig; protected defaultEnvironment?: EnvironmentConfig;
/** /**
@ -67,7 +67,7 @@ export class CoreConfigProvider {
await table.initialize(); await table.initialize();
this.table.resolve(table); this.table.setInstance(table);
} }
/** /**
@ -77,9 +77,7 @@ export class CoreConfigProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async delete(name: string): Promise<void> { async delete(name: string): Promise<void> {
const table = await this.table; await this.table.deleteByPrimaryKey({ name });
await table.deleteByPrimaryKey({ name });
} }
/** /**
@ -91,8 +89,7 @@ export class CoreConfigProvider {
*/ */
async get<T>(name: string, defaultValue?: T): Promise<T> { async get<T>(name: string, defaultValue?: T): Promise<T> {
try { try {
const table = await this.table; const record = await this.table.findByPrimaryKey({ name });
const record = await table.findByPrimaryKey({ name });
return record.value; return record.value;
} catch (error) { } catch (error) {
@ -112,9 +109,7 @@ export class CoreConfigProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async set(name: string, value: number | string): Promise<void> { async set(name: string, value: number | string): Promise<void> {
const table = await this.table; await this.table.insert({ name, value });
await table.insert({ name, value });
} }
/** /**

View File

@ -0,0 +1,122 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CorePromisedValue } from '@classes/promised-value';
/**
* Create a wrapper to hold an asynchronous instance.
*
* @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance wrapper.
*/
function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstanceWrapper<T> {
let promisedInstance: CorePromisedValue<T> | null = null;
return {
get instance() {
return promisedInstance?.value ?? undefined;
},
async getInstance() {
if (!promisedInstance) {
promisedInstance = new CorePromisedValue();
if (lazyConstructor) {
const instance = await lazyConstructor();
promisedInstance.resolve(instance);
}
}
return promisedInstance;
},
async getProperty(property) {
const instance = await this.getInstance();
return instance[property];
},
setInstance(instance) {
if (!promisedInstance) {
promisedInstance = new CorePromisedValue();
} else if (promisedInstance.isSettled()) {
promisedInstance.reset();
}
promisedInstance.resolve(instance);
},
resetInstance() {
if (!promisedInstance) {
return;
}
promisedInstance.reset();
},
};
}
/**
* Asynchronous instance wrapper.
*/
export interface AsyncInstanceWrapper<T> {
instance?: T;
getInstance(): Promise<T>;
getProperty<P extends keyof T>(property: P): Promise<T[P]>;
setInstance(instance: T): void;
resetInstance(): void;
}
/**
* Asynchronous version of a method.
*/
export type AsyncMethod<T> =
T extends (...args: infer Params) => infer Return
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? T extends (...args: Params) => Promise<any>
? T
: (...args: Params) => Promise<Return>
: never;
/**
* Asynchronous instance.
*
* All methods are converted to their asynchronous version, and properties are available asynchronously using
* the getProperty method.
*/
export type AsyncInstance<T> = AsyncInstanceWrapper<T> & {
[k in keyof T]: AsyncMethod<T[k]>;
};
/**
* Create an asynchronous instance proxy, where all methods will be callable directly but will become asynchronous. If the
* underlying instance hasn't been set, methods will be resolved once it is.
*
* @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance.
*/
export function asyncInstance<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstance<T> {
const wrapper = createAsyncInstanceWrapper<T>(lazyConstructor);
return new Proxy(wrapper, {
get: (target, property, receiver) => {
if (property in target) {
return Reflect.get(target, property, receiver);
}
return async (...args: unknown[]) => {
const instance = await wrapper.getInstance();
return instance[property](...args);
};
},
}) as AsyncInstance<T>;
}