forked from EVOgeek/Vmeda.Online
		
	MOBILE-3971 core: Improve config db performance
This commit is contained in:
		
							parent
							
								
									1d8f0c5a66
								
							
						
					
					
						commit
						cca8c0a530
					
				
							
								
								
									
										194
									
								
								src/core/classes/database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/core/classes/database-table.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | |||||||
|  | // (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 { SQLiteDB, SQLiteDBRecordValues } from './sqlitedb'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Database table wrapper used to improve performance by caching all data in memory | ||||||
|  |  * for faster read operations. | ||||||
|  |  */ | ||||||
|  | export abstract class CoreDatabaseTable< | ||||||
|  |     DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, | ||||||
|  |     PrimaryKeyColumns extends keyof DBRecord = 'id', | ||||||
|  |     PrimaryKey extends GetPrimaryKey<DBRecord, PrimaryKeyColumns> = GetPrimaryKey<DBRecord, PrimaryKeyColumns> | ||||||
|  | > { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create an instance. | ||||||
|  |      * | ||||||
|  |      * @param db Database connection. | ||||||
|  |      * @returns Instance. | ||||||
|  |      */ | ||||||
|  |     static async create<This extends AnyCoreDatabaseTable>(this: CoreDatabaseTableConstructor<This>, db: SQLiteDB): Promise<This> { | ||||||
|  |         const instance = new this(db); | ||||||
|  | 
 | ||||||
|  |         await instance.initialize(); | ||||||
|  | 
 | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected db: SQLiteDB; | ||||||
|  |     protected data: Record<string, DBRecord>; | ||||||
|  |     protected primaryKeys: string[] = ['id']; | ||||||
|  | 
 | ||||||
|  |     constructor(db: SQLiteDB) { | ||||||
|  |         this.db = db; | ||||||
|  |         this.data = {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a record matching the given conditions. | ||||||
|  |      * | ||||||
|  |      * @param conditions Matching conditions. | ||||||
|  |      * @returns Database record. | ||||||
|  |      */ | ||||||
|  |     find(conditions: Partial<DBRecord>): DBRecord | null  { | ||||||
|  |         return Object.values(this.data).find(record => this.recordMatches(record, conditions)) ?? null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a record by its primary key. | ||||||
|  |      * | ||||||
|  |      * @param primaryKey Primary key. | ||||||
|  |      * @returns Database record. | ||||||
|  |      */ | ||||||
|  |     findByPrimaryKey(primaryKey: PrimaryKey): DBRecord | null { | ||||||
|  |         return this.data[this.serializePrimaryKey(primaryKey)] ?? null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Insert a new record. | ||||||
|  |      * | ||||||
|  |      * @param record Database record. | ||||||
|  |      */ | ||||||
|  |     async insert(record: DBRecord): Promise<void> { | ||||||
|  |         await this.db.insertRecord(this.table, record); | ||||||
|  | 
 | ||||||
|  |         const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); | ||||||
|  | 
 | ||||||
|  |         this.data[primaryKey] = record; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete records matching the given conditions. | ||||||
|  |      * | ||||||
|  |      * @param conditions Matching conditions. If this argument is missing, all records will be deleted. | ||||||
|  |      */ | ||||||
|  |     async delete(conditions?: Partial<DBRecord>): Promise<void> { | ||||||
|  |         if (!conditions) { | ||||||
|  |             await this.db.deleteRecords(this.table); | ||||||
|  | 
 | ||||||
|  |             this.data = {}; | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await this.db.deleteRecords(this.table, conditions); | ||||||
|  | 
 | ||||||
|  |         Object.entries(this.data).forEach(([id, record]) => { | ||||||
|  |             if (!this.recordMatches(record, conditions)) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             delete this.data[id]; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete a single record identified by its primary key. | ||||||
|  |      * | ||||||
|  |      * @param primaryKey Record primary key. | ||||||
|  |      */ | ||||||
|  |     async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> { | ||||||
|  |         await this.db.deleteRecords(this.table, primaryKey); | ||||||
|  | 
 | ||||||
|  |         delete this.data[this.serializePrimaryKey(primaryKey)]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Database table name. | ||||||
|  |      */ | ||||||
|  |     protected abstract get table(): string; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize object by getting the current state of the database table. | ||||||
|  |      */ | ||||||
|  |     protected async initialize(): Promise<void> { | ||||||
|  |         const records = await this.db.getRecords<DBRecord>(this.table); | ||||||
|  | 
 | ||||||
|  |         this.data = records.reduce((data, record) => { | ||||||
|  |             const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); | ||||||
|  | 
 | ||||||
|  |             data[primaryKey] = record; | ||||||
|  | 
 | ||||||
|  |             return data; | ||||||
|  |         }, {}); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get an object with the columns representing the primary key of a database record. | ||||||
|  |      * | ||||||
|  |      * @param record Database record. | ||||||
|  |      * @returns Primary key column-value pairs. | ||||||
|  |      */ | ||||||
|  |     protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey { | ||||||
|  |         return this.primaryKeys.reduce((primaryKey, column) => { | ||||||
|  |             primaryKey[column] = record[column]; | ||||||
|  | 
 | ||||||
|  |             return primaryKey; | ||||||
|  |         }, {} as Record<PrimaryKeyColumns, unknown>) as PrimaryKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Serialize a primary key with a string representation. | ||||||
|  |      * | ||||||
|  |      * @param primaryKey Database record primary key. | ||||||
|  |      * @returns Serialized primary key. | ||||||
|  |      */ | ||||||
|  |     protected serializePrimaryKey(primaryKey: PrimaryKey): string { | ||||||
|  |         return Object.values(primaryKey).map(value => String(value)).join('-'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether a given record matches the given conditions. | ||||||
|  |      * | ||||||
|  |      * @param record Database record. | ||||||
|  |      * @param conditions Conditions. | ||||||
|  |      * @returns Whether the record matches the conditions. | ||||||
|  |      */ | ||||||
|  |     protected recordMatches(record: DBRecord, conditions: Partial<DBRecord>): boolean { | ||||||
|  |         return !Object.entries(conditions).some(([column, value]) => record[column] !== value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Generic type to match against any concrete database table type. | ||||||
|  |  */ | ||||||
|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  | type AnyCoreDatabaseTable = CoreDatabaseTable<SQLiteDBRecordValues, string, Record<string, any>>; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Database table constructor. | ||||||
|  |  */ | ||||||
|  | type CoreDatabaseTableConstructor<T extends AnyCoreDatabaseTable> = { | ||||||
|  |     new (db: SQLiteDB): T; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Infer primary key type from database record and columns types. | ||||||
|  |  */ | ||||||
|  | type GetPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumns extends keyof DBRecord> = { | ||||||
|  |     [column in PrimaryKeyColumns]: DBRecord[column]; | ||||||
|  | }; | ||||||
							
								
								
									
										124
									
								
								src/core/classes/tests/database-table.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/core/classes/tests/database-table.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | // (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 { mock } from '@/testing/utils'; | ||||||
|  | import { CoreDatabaseTable } from '@classes/database-table'; | ||||||
|  | import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; | ||||||
|  | 
 | ||||||
|  | interface User extends SQLiteDBRecordValues { | ||||||
|  |     id: number; | ||||||
|  |     name: string; | ||||||
|  |     surname: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class UsersTable extends CoreDatabaseTable<User> { | ||||||
|  | 
 | ||||||
|  |     protected table = 'users'; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | describe('CoreDatabaseTable', () => { | ||||||
|  | 
 | ||||||
|  |     let records: User[]; | ||||||
|  |     let db: SQLiteDB; | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         records = []; | ||||||
|  |         db = mock<SQLiteDB>({ | ||||||
|  |             getRecords: async <T>() => records as unknown as T[], | ||||||
|  |             deleteRecords: async () => 0, | ||||||
|  |             insertRecord: async () => 0, | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('reads all records on create', async () => { | ||||||
|  |         await UsersTable.create(db); | ||||||
|  | 
 | ||||||
|  |         expect(db.getRecords).toHaveBeenCalledWith('users'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('finds items', async () => { | ||||||
|  |         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||||
|  |         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||||
|  | 
 | ||||||
|  |         records.push(john); | ||||||
|  |         records.push(amy); | ||||||
|  | 
 | ||||||
|  |         const table = await UsersTable.create(db); | ||||||
|  | 
 | ||||||
|  |         expect(table.findByPrimaryKey({ id: 1 })).toEqual(john); | ||||||
|  |         expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy); | ||||||
|  |         expect(table.find({ surname: 'Doe', name: 'John' })).toEqual(john); | ||||||
|  |         expect(table.find({ surname: 'Doe', name: 'Amy' })).toEqual(amy); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('inserts items', async () => { | ||||||
|  |         // Arrange.
 | ||||||
|  |         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||||
|  | 
 | ||||||
|  |         // Act.
 | ||||||
|  |         const table = await UsersTable.create(db); | ||||||
|  | 
 | ||||||
|  |         await table.insert(john); | ||||||
|  | 
 | ||||||
|  |         // Assert.
 | ||||||
|  |         expect(db.insertRecord).toHaveBeenCalledWith('users', john); | ||||||
|  | 
 | ||||||
|  |         expect(table.findByPrimaryKey({ id: 1 })).toEqual(john); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('deletes items', async () => { | ||||||
|  |         // Arrange.
 | ||||||
|  |         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||||
|  |         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||||
|  |         const jane = { id: 3, name: 'Jane', surname: 'Smith' }; | ||||||
|  | 
 | ||||||
|  |         records.push(john); | ||||||
|  |         records.push(amy); | ||||||
|  |         records.push(jane); | ||||||
|  | 
 | ||||||
|  |         // Act.
 | ||||||
|  |         const table = await UsersTable.create(db); | ||||||
|  | 
 | ||||||
|  |         await table.delete({ surname: 'Doe' }); | ||||||
|  | 
 | ||||||
|  |         // Assert.
 | ||||||
|  |         expect(db.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' }); | ||||||
|  | 
 | ||||||
|  |         expect(table.findByPrimaryKey({ id: 1 })).toBeNull(); | ||||||
|  |         expect(table.findByPrimaryKey({ id: 2 })).toBeNull(); | ||||||
|  |         expect(table.findByPrimaryKey({ id: 3 })).toEqual(jane); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('deletes items by primary key', async () => { | ||||||
|  |         // Arrange.
 | ||||||
|  |         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||||
|  |         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||||
|  | 
 | ||||||
|  |         records.push(john); | ||||||
|  |         records.push(amy); | ||||||
|  | 
 | ||||||
|  |         // Act.
 | ||||||
|  |         const table = await UsersTable.create(db); | ||||||
|  | 
 | ||||||
|  |         await table.deleteByPrimaryKey({ id: 1 }); | ||||||
|  | 
 | ||||||
|  |         // Assert.
 | ||||||
|  |         expect(db.deleteRecords).toHaveBeenCalledWith('users', { id: 1 }); | ||||||
|  | 
 | ||||||
|  |         expect(table.findByPrimaryKey({ id: 1 })).toBeNull(); | ||||||
|  |         expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  | }); | ||||||
| @ -15,9 +15,10 @@ | |||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { SQLiteDB } from '@classes/sqlitedb'; |  | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config'; | import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config'; | ||||||
|  | import { CoreDatabaseTable } from '@classes/database-table'; | ||||||
|  | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Factory to provide access to dynamic and permanent config and settings. |  * Factory to provide access to dynamic and permanent config and settings. | ||||||
| @ -26,11 +27,10 @@ import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database | |||||||
| @Injectable({ providedIn: 'root' }) | @Injectable({ providedIn: 'root' }) | ||||||
| export class CoreConfigProvider { | export class CoreConfigProvider { | ||||||
| 
 | 
 | ||||||
|     protected appDB: Promise<SQLiteDB>; |     protected dbTable: CorePromisedValue<CoreConfigTable>; | ||||||
|     protected resolveAppDB!: (appDB: SQLiteDB) => void; |  | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.appDB = new Promise(resolve => this.resolveAppDB = resolve); |         this.dbTable = new CorePromisedValue(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -43,7 +43,10 @@ export class CoreConfigProvider { | |||||||
|             // Ignore errors.
 |             // Ignore errors.
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.resolveAppDB(CoreApp.getDB()); |         const db = CoreApp.getDB(); | ||||||
|  |         const table = await CoreConfigTable.create(db); | ||||||
|  | 
 | ||||||
|  |         this.dbTable.resolve(table); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -53,9 +56,9 @@ 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 db = await this.appDB; |         const table = await this.dbTable; | ||||||
| 
 | 
 | ||||||
|         await db.deleteRecords(CONFIG_TABLE_NAME, { name }); |         await table.deleteByPrimaryKey({ name }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -66,19 +69,18 @@ export class CoreConfigProvider { | |||||||
|      * @return Resolves upon success along with the config data. Reject on failure. |      * @return Resolves upon success along with the config data. Reject on failure. | ||||||
|      */ |      */ | ||||||
|     async get<T>(name: string, defaultValue?: T): Promise<T> { |     async get<T>(name: string, defaultValue?: T): Promise<T> { | ||||||
|         const db = await this.appDB; |         const table = await this.dbTable; | ||||||
|  |         const record = table.findByPrimaryKey({ name }); | ||||||
| 
 | 
 | ||||||
|         try { |         if (record !== null) { | ||||||
|             const entry = await db.getRecord<ConfigDBEntry>(CONFIG_TABLE_NAME, { name }); |             return record.value; | ||||||
| 
 |  | ||||||
|             return entry.value; |  | ||||||
|         } catch (error) { |  | ||||||
|             if (defaultValue !== undefined) { |  | ||||||
|                 return defaultValue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             throw error; |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (defaultValue !== undefined) { | ||||||
|  |             return defaultValue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         throw new Error(`Couldn't get config with name '${name}'`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -89,11 +91,21 @@ 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 db = await this.appDB; |         const table = await this.dbTable; | ||||||
| 
 | 
 | ||||||
|         await db.insertRecord(CONFIG_TABLE_NAME, { name, value }); |         await table.insert({ name, value }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const CoreConfig = makeSingleton(CoreConfigProvider); | export const CoreConfig = makeSingleton(CoreConfigProvider); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Config database table. | ||||||
|  |  */ | ||||||
|  | class CoreConfigTable extends CoreDatabaseTable<ConfigDBEntry, 'name'> { | ||||||
|  | 
 | ||||||
|  |     protected table = CONFIG_TABLE_NAME; | ||||||
|  |     protected primaryKeys = ['name']; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | |||||||
| @ -133,12 +133,10 @@ export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>; | |||||||
|  * @param overrides Object with the properties or methods to override, or a list of methods to override with an empty function. |  * @param overrides Object with the properties or methods to override, or a list of methods to override with an empty function. | ||||||
|  * @return Mock instance. |  * @return Mock instance. | ||||||
|  */ |  */ | ||||||
| export function mock<T>(instance?: T, overrides?: string[] | Record<string, unknown>): T; |  | ||||||
| export function mock<T>(instance?: Partial<T>, overrides?: string[] | Record<string, unknown>): Partial<T>; |  | ||||||
| export function mock<T>( | export function mock<T>( | ||||||
|     instance: T | Partial<T> = {}, |     instance: T | Partial<T> = {}, | ||||||
|     overrides: string[] | Record<string, unknown> = {}, |     overrides: string[] | Record<string, unknown> = {}, | ||||||
| ): T | Partial<T> { | ): T { | ||||||
|     // If overrides is an object, apply them to the instance.
 |     // If overrides is an object, apply them to the instance.
 | ||||||
|     if (!Array.isArray(overrides)) { |     if (!Array.isArray(overrides)) { | ||||||
|         Object.assign(instance, overrides); |         Object.assign(instance, overrides); | ||||||
| @ -162,7 +160,7 @@ export function mock<T>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return instance; |     return instance as T; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mockSingleton<T>(singletonClass: CoreSingletonProxy<T>, instance: T): T; | export function mockSingleton<T>(singletonClass: CoreSingletonProxy<T>, instance: T): T; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user