From 7a2a8c3e9867f65d81151f39c01b77ce9281dcd3 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 3 Feb 2022 13:56:36 +0100 Subject: [PATCH] MOBILE-3977 core: Implement async instance pattern --- .../classes/database/database-table-proxy.ts | 46 ++----- src/core/classes/site.ts | 41 +++--- src/core/services/config.ts | 17 +-- src/core/utils/async-instance.ts | 122 ++++++++++++++++++ 4 files changed, 158 insertions(+), 68 deletions(-) create mode 100644 src/core/utils/async-instance.ts diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index 330640e9c..0a9006282 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -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 { protected config: CoreDatabaseConfiguration; - protected target: CorePromisedValue> = new CorePromisedValue(); + protected target = asyncInstance>(); protected environmentObserver?: CoreEventObserver; constructor( @@ -68,81 +68,63 @@ export class CoreDatabaseTableProxy< * @inheritdoc */ async all(conditions?: Partial): Promise { - const target = await this.target; - - return target.all(conditions); + return this.target.all(conditions); } /** * @inheritdoc */ async find(conditions: Partial): Promise { - const target = await this.target; - - return target.find(conditions); + return this.target.find(conditions); } /** * @inheritdoc */ async findByPrimaryKey(primaryKey: PrimaryKey): Promise { - const target = await this.target; - - return target.findByPrimaryKey(primaryKey); + return this.target.findByPrimaryKey(primaryKey); } /** * @inheritdoc */ async reduce(reducer: CoreDatabaseReducer, conditions?: CoreDatabaseConditions): Promise { - const target = await this.target; - - return target.reduce(reducer, conditions); + return this.target.reduce(reducer, conditions); } /** * @inheritdoc */ async insert(record: DBRecord): Promise { - const target = await this.target; - - return target.insert(record); + return this.target.insert(record); } /** * @inheritdoc */ async update(updates: Partial, conditions?: Partial): Promise { - const target = await this.target; - - return target.update(updates, conditions); + return this.target.update(updates, conditions); } /** * @inheritdoc */ async updateWhere(updates: Partial, conditions: CoreDatabaseConditions): Promise { - const target = await this.target; - - return target.updateWhere(updates, conditions); + return this.target.updateWhere(updates, conditions); } /** * @inheritdoc */ async delete(conditions?: Partial): Promise { - const target = await this.target; - - return target.delete(conditions); + return this.target.delete(conditions); } /** * @inheritdoc */ async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise { - 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 { - 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); } /** diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 3182f60ff..5ce95a266 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -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>; 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 { const params: Array = [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 { 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 { - 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, diff --git a/src/core/services/config.ts b/src/core/services/config.ts index d804535f1..0ed248301 100644 --- a/src/core/services/config.ts +++ b/src/core/services/config.ts @@ -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> = new CorePromisedValue(); + protected table = asyncInstance>(); 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 { - const table = await this.table; - - await table.deleteByPrimaryKey({ name }); + await this.table.deleteByPrimaryKey({ name }); } /** @@ -91,8 +89,7 @@ export class CoreConfigProvider { */ async get(name: string, defaultValue?: T): Promise { 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 { - const table = await this.table; - - await table.insert({ name, value }); + await this.table.insert({ name, value }); } /** diff --git a/src/core/utils/async-instance.ts b/src/core/utils/async-instance.ts new file mode 100644 index 000000000..f687e8a70 --- /dev/null +++ b/src/core/utils/async-instance.ts @@ -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(lazyConstructor?: () => T | Promise): AsyncInstanceWrapper { + let promisedInstance: CorePromisedValue | 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 { + instance?: T; + getInstance(): Promise; + getProperty

(property: P): Promise; + setInstance(instance: T): void; + resetInstance(): void; +} + +/** + * Asynchronous version of a method. + */ +export type AsyncMethod = + T extends (...args: infer Params) => infer Return + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ? T extends (...args: Params) => Promise + ? T + : (...args: Params) => Promise + : never; + +/** + * Asynchronous instance. + * + * All methods are converted to their asynchronous version, and properties are available asynchronously using + * the getProperty method. + */ +export type AsyncInstance = AsyncInstanceWrapper & { + [k in keyof T]: AsyncMethod; +}; + +/** + * 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(lazyConstructor?: () => T | Promise): AsyncInstance { + const wrapper = createAsyncInstanceWrapper(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; +}