MOBILE-3977 core: Implement async instance pattern
This commit is contained in:
		
							parent
							
								
									a041471205
								
							
						
					
					
						commit
						7a2a8c3e98
					
				| @ -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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
							
								
								
									
										122
									
								
								src/core/utils/async-instance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/core/utils/async-instance.ts
									
									
									
									
									
										Normal 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>; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user