MOBILE-3977 core: Implement async instance pattern
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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…
Reference in New Issue