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.
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 { CoreConfigProvider } from '@services/config';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
@ -34,7 +34,7 @@ export class CoreDatabaseTableProxy<
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
protected config: CoreDatabaseConfiguration;
protected target: CorePromisedValue<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> = new CorePromisedValue();
protected target = asyncInstance<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>();
protected environmentObserver?: CoreEventObserver;
constructor(
@ -68,81 +68,63 @@ export class CoreDatabaseTableProxy<
* @inheritdoc
*/
async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
const target = await this.target;
return target.all(conditions);
return this.target.all(conditions);
}
/**
* @inheritdoc
*/
async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
const target = await this.target;
return target.find(conditions);
return this.target.find(conditions);
}
/**
* @inheritdoc
*/
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
const target = await this.target;
return target.findByPrimaryKey(primaryKey);
return this.target.findByPrimaryKey(primaryKey);
}
/**
* @inheritdoc
*/
async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
const target = await this.target;
return target.reduce<T>(reducer, conditions);
return this.target.reduce<T>(reducer, conditions);
}
/**
* @inheritdoc
*/
async insert(record: DBRecord): Promise<void> {
const target = await this.target;
return target.insert(record);
return this.target.insert(record);
}
/**
* @inheritdoc
*/
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
const target = await this.target;
return target.update(updates, conditions);
return this.target.update(updates, conditions);
}
/**
* @inheritdoc
*/
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
const target = await this.target;
return target.updateWhere(updates, conditions);
return this.target.updateWhere(updates, conditions);
}
/**
* @inheritdoc
*/
async delete(conditions?: Partial<DBRecord>): Promise<void> {
const target = await this.target;
return target.delete(conditions);
return this.target.delete(conditions);
}
/**
* @inheritdoc
*/
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
const target = await this.target;
return target.deleteByPrimaryKey(primaryKey);
return this.target.deleteByPrimaryKey(primaryKey);
}
/**
@ -174,18 +156,18 @@ export class CoreDatabaseTableProxy<
* Update underlying target instance.
*/
protected async updateTarget(): Promise<void> {
const oldTarget = this.target.value;
const oldTarget = this.target.instance;
const newTarget = this.createTarget();
if (oldTarget) {
await oldTarget.destroy();
this.target.reset();
this.target.resetInstance();
}
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 { CoreLang } from '@services/lang';
import { CoreSites } from '@services/sites';
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CoreDatabaseTable } from './database/database-table';
/**
* QR Code type enumeration.
@ -104,6 +106,7 @@ export class CoreSite {
// Rest of variables.
protected logger: CoreLogger;
protected db?: SQLiteDB;
protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
protected cleanUnicode = false;
protected lastAutoLogin = 0;
protected offlineDisabled = false;
@ -137,6 +140,7 @@ export class CoreSite {
) {
this.logger = CoreLogger.getInstance('CoreSite');
this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
this.cacheTable = asyncInstance(() => CoreSites.getCacheTable(this));
this.setInfo(infos);
this.calculateOfflineDisabled();
@ -926,15 +930,14 @@ export class CoreSite {
}
const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
let entry: CoreSiteWSCacheRecord | undefined;
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) {
// Cache key not found, get by params sent.
entry = await cacheTable.findByPrimaryKey({ id });
entry = await this.cacheTable.findByPrimaryKey({ id });
} else {
if (entries.length > 1) {
// More than one entry found. Search the one with same ID as this call.
@ -946,7 +949,7 @@ export class CoreSite {
}
}
} else {
entry = await cacheTable.findByPrimaryKey({ id });
entry = await this.cacheTable.findByPrimaryKey({ id });
}
if (entry === undefined) {
@ -991,14 +994,13 @@ export class CoreSite {
*/
async getComponentCacheSize(component: string, componentId?: number): Promise<number> {
const params: Array<string | number> = [component];
const cacheTable = await CoreSites.getCacheTable(this);
let extraClause = '';
if (componentId !== undefined && componentId !== null) {
params.push(componentId);
extraClause = ' AND componentId = ?';
}
return cacheTable.reduce(
return this.cacheTable.reduce(
{
sql: 'SUM(length(data))',
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.
// We decided to reuse this field to prevent modifying the database table.
const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
const entry = {
id,
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
protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> {
const id = this.getCacheId(method, data);
const cacheTable = await CoreSites.getCacheTable(this);
if (allCacheKey) {
await cacheTable.delete({ key: preSets.cacheKey });
await this.cacheTable.delete({ key: preSets.cacheKey });
} else {
await cacheTable.deleteByPrimaryKey({ id });
await this.cacheTable.deleteByPrimaryKey({ id });
}
}
@ -1087,13 +1087,12 @@ export class CoreSite {
}
const params = { component };
const cacheTable = await CoreSites.getCacheTable(this);
if (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);
try {
const cacheTable = await CoreSites.getCacheTable(this);
await cacheTable.update({ expirationTime: 0 });
await this.cacheTable.update({ expirationTime: 0 });
} finally {
CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
}
@ -1149,9 +1146,7 @@ export class CoreSite {
this.logger.debug('Invalidate cache for key: ' + key);
const cacheTable = await CoreSites.getCacheTable(this);
await cacheTable.update({ expirationTime: 0 }, { key });
await this.cacheTable.update({ expirationTime: 0 }, { key });
}
/**
@ -1185,9 +1180,7 @@ export class CoreSite {
this.logger.debug('Invalidate cache for key starting with: ' + key);
const cacheTable = await CoreSites.getCacheTable(this);
await cacheTable.updateWhere({ expirationTime: 0 }, {
await this.cacheTable.updateWhere({ expirationTime: 0 }, {
sql: 'key LIKE ?',
sqlParams: [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)
*/
async getCacheUsage(): Promise<number> {
const cacheTable = await CoreSites.getCacheTable(this);
return cacheTable.reduce({
return this.cacheTable.reduce({
sql: 'SUM(length(data))',
js: (size, record) => size + record.data.length,
jsInitialValue: 0,

View File

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