MOBILE-3977 core: Reuse databases initialization

main
Noel De Martin 2022-02-07 13:46:09 +01:00
parent 7a2a8c3e98
commit 359d7ab5a5
4 changed files with 114 additions and 82 deletions

View File

@ -44,6 +44,7 @@ import { CoreLang } from '@services/lang';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CoreDatabaseTable } from './database/database-table'; import { CoreDatabaseTable } from './database/database-table';
import { CoreDatabaseCachingStrategy } from './database/database-table-proxy';
/** /**
* QR Code type enumeration. * QR Code type enumeration.
@ -140,7 +141,11 @@ export class CoreSite {
) { ) {
this.logger = CoreLogger.getInstance('CoreSite'); this.logger = CoreLogger.getInstance('CoreSite');
this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params. this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
this.cacheTable = asyncInstance(() => CoreSites.getCacheTable(this)); this.cacheTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.WS_CACHE_TABLE, {
siteId: this.getId(),
database: this.getDb(),
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
}));
this.setInfo(infos); this.setInfo(infos);
this.calculateOfflineDisabled(); this.calculateOfflineDisabled();

View File

@ -48,9 +48,10 @@ import {
} from '@services/database/filepool'; } from '@services/database/filepool';
import { CoreFileHelper } from './file-helper'; import { CoreFileHelper } from './file-helper';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
import { lazyMap, LazyMap } from '../utils/lazy-map';
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
/* /*
* Factory for handling downloading files and retrieve downloaded files. * Factory for handling downloading files and retrieve downloaded files.
@ -101,11 +102,20 @@ export class CoreFilepoolProvider {
// Variables for DB. // Variables for DB.
protected appDB: Promise<SQLiteDB>; protected appDB: Promise<SQLiteDB>;
protected resolveAppDB!: (appDB: SQLiteDB) => void; protected resolveAppDB!: (appDB: SQLiteDB) => void;
protected filesTables: Record<string, CorePromisedValue<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>> = {}; protected filesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>>;
constructor() { constructor() {
this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); this.logger = CoreLogger.getInstance('CoreFilepoolProvider');
this.filesTables = lazyMap(
siteId => asyncInstance(
() => CoreSites.getSiteTable<CoreFilepoolFileEntry, 'fileId'>(FILES_TABLE_NAME, {
siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
primaryKeyColumns: ['fileId'],
}),
),
);
} }
/** /**
@ -128,11 +138,9 @@ export class CoreFilepoolProvider {
return; return;
} }
const filesTable = await this.filesTables[siteId]; await this.filesTables[siteId].destroy();
delete this.filesTables[siteId]; delete this.filesTables[siteId];
await filesTable.destroy();
}); });
} }
@ -149,33 +157,6 @@ export class CoreFilepoolProvider {
this.resolveAppDB(CoreApp.getDB()); this.resolveAppDB(CoreApp.getDB());
} }
/**
* Get files table.
*
* @param siteId Site id.
* @returns Files table.
*/
async getFilesTable(siteId?: string): Promise<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>> {
siteId = siteId ?? CoreSites.getCurrentSiteId();
if (!(siteId in this.filesTables)) {
const filesTable = this.filesTables[siteId] = new CorePromisedValue();
const database = await CoreSites.getSiteDb(siteId);
const table = new CoreDatabaseTableProxy<CoreFilepoolFileEntry, 'fileId'>(
{ cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
database,
FILES_TABLE_NAME,
['fileId'],
);
await table.initialize();
filesTable.resolve(table);
}
return this.filesTables[siteId];
}
/** /**
* Link a file with a component. * Link a file with a component.
* *
@ -262,9 +243,7 @@ export class CoreFilepoolProvider {
...data, ...data,
}; };
const filesTable = await this.getFilesTable(siteId); await this.filesTables[siteId].insert(record);
await filesTable.insert(record);
} }
/** /**
@ -605,14 +584,13 @@ export class CoreFilepoolProvider {
*/ */
async clearFilepool(siteId: string): Promise<void> { async clearFilepool(siteId: string): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
// Read the data first to be able to notify the deletions. // Read the data first to be able to notify the deletions.
const filesEntries = await filesTable.all(); const filesEntries = await this.filesTables[siteId].all();
const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME); const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME);
await Promise.all([ await Promise.all([
filesTable.delete(), this.filesTables[siteId].delete(),
db.deleteRecords(LINKS_TABLE_NAME), db.deleteRecords(LINKS_TABLE_NAME),
]); ]);
@ -1167,14 +1145,13 @@ export class CoreFilepoolProvider {
} }
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const extension = CoreMimetypeUtils.getFileExtension(entry.path); const extension = CoreMimetypeUtils.getFileExtension(entry.path);
if (!extension) { if (!extension) {
// Files does not have extension. Invalidate file (stale = true). // Files does not have extension. Invalidate file (stale = true).
// Minor problem: file will remain in the filesystem once downloaded again. // Minor problem: file will remain in the filesystem once downloaded again.
this.logger.debug('Staled file with no extension ' + entry.fileId); this.logger.debug('Staled file with no extension ' + entry.fileId);
await filesTable.update({ stale: 1 }, { fileId: entry.fileId }); await this.filesTables[siteId].update({ stale: 1 }, { fileId: entry.fileId });
return; return;
} }
@ -1184,7 +1161,7 @@ export class CoreFilepoolProvider {
entry.fileId = CoreMimetypeUtils.removeExtension(fileId); entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
entry.extension = extension; entry.extension = extension;
await filesTable.update(entry, { fileId }); await this.filesTables[siteId].update(entry, { fileId });
if (entry.fileId == fileId) { if (entry.fileId == fileId) {
// File ID hasn't changed, we're done. // File ID hasn't changed, we're done.
this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId); this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
@ -1445,13 +1422,12 @@ export class CoreFilepoolProvider {
*/ */
async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> { async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const items = await this.getComponentFiles(db, component, componentId); const items = await this.getComponentFiles(db, component, componentId);
const files: CoreFilepoolFileEntry[] = []; const files: CoreFilepoolFileEntry[] = [];
await Promise.all(items.map(async (item) => { await Promise.all(items.map(async (item) => {
try { try {
const fileEntry = await filesTable.findByPrimaryKey({ fileId: item.fileId }); const fileEntry = await this.filesTables[siteId].findByPrimaryKey({ fileId: item.fileId });
if (!fileEntry) { if (!fileEntry) {
return; return;
@ -2184,9 +2160,7 @@ export class CoreFilepoolProvider {
* @return Resolved with file object from DB on success, rejected otherwise. * @return Resolved with file object from DB on success, rejected otherwise.
*/ */
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> { protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
const filesTable = await this.getFilesTable(siteId); return this.filesTables[siteId].findByPrimaryKey({ fileId });
return filesTable.findByPrimaryKey({ fileId });
} }
/** /**
@ -2218,17 +2192,15 @@ export class CoreFilepoolProvider {
* @return Resolved on success. * @return Resolved on success.
*/ */
async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> { async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
const filesTable = await this.getFilesTable(siteId);
onlyUnknown onlyUnknown
? await filesTable.updateWhere( ? await this.filesTables[siteId].updateWhere(
{ stale: 1 }, { stale: 1 },
{ {
sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL, sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL,
js: CoreFilepoolProvider.FILE_IS_UNKNOWN_JS, js: CoreFilepoolProvider.FILE_IS_UNKNOWN_JS,
}, },
) )
: await filesTable.update({ stale: 1 }); : await this.filesTables[siteId].update({ stale: 1 });
} }
/** /**
@ -2247,9 +2219,7 @@ export class CoreFilepoolProvider {
const file = await this.fixPluginfileURL(siteId, fileUrl); const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
const filesTable = await this.getFilesTable(siteId); await this.filesTables[siteId].update({ stale: 1 }, { fileId });
await filesTable.update({ stale: 1 }, { fileId });
} }
/** /**
@ -2269,7 +2239,6 @@ export class CoreFilepoolProvider {
onlyUnknown: boolean = true, onlyUnknown: boolean = true,
): Promise<void> { ): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
const items = await this.getComponentFiles(db, component, componentId); const items = await this.getComponentFiles(db, component, componentId);
if (!items.length) { if (!items.length) {
@ -2277,6 +2246,8 @@ export class CoreFilepoolProvider {
return; return;
} }
siteId = siteId ?? CoreSites.getCurrentSiteId();
const fileIds = items.map((item) => item.fileId); const fileIds = items.map((item) => item.fileId);
const whereAndParams = db.getInOrEqual(fileIds); const whereAndParams = db.getInOrEqual(fileIds);
@ -2287,7 +2258,7 @@ export class CoreFilepoolProvider {
whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')'; whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')';
} }
await filesTable.updateWhere( await this.filesTables[siteId].updateWhere(
{ stale: 1 }, { stale: 1 },
{ {
sql: whereAndParams.sql, sql: whereAndParams.sql,
@ -2714,7 +2685,6 @@ export class CoreFilepoolProvider {
*/ */
protected async removeFileById(siteId: string, fileId: string): Promise<void> { protected async removeFileById(siteId: string, fileId: string): Promise<void> {
const db = await CoreSites.getSiteDb(siteId); const db = await CoreSites.getSiteDb(siteId);
const filesTable = await this.getFilesTable(siteId);
// Get the path to the file first since it relies on the file object stored in the pool. // Get the path to the file first since it relies on the file object stored in the pool.
// Don't use getFilePath to prevent performing 2 DB requests. // Don't use getFilePath to prevent performing 2 DB requests.
@ -2741,7 +2711,7 @@ export class CoreFilepoolProvider {
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
// Remove entry from filepool store. // Remove entry from filepool store.
promises.push(filesTable.delete(conditions)); promises.push(this.filesTables[siteId].delete(conditions));
// Remove links. // Remove links.
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions)); promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));

View File

@ -31,9 +31,8 @@ import {
CoreSiteConfig, CoreSiteConfig,
CoreSitePublicConfigResponse, CoreSitePublicConfigResponse,
CoreSiteInfoResponse, CoreSiteInfoResponse,
CoreSiteWSCacheRecord,
} from '@classes/site'; } from '@classes/site';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreSiteError } from '@classes/errors/siteerror';
import { makeSingleton, Translate, Http } from '@singletons'; import { makeSingleton, Translate, Http } from '@singletons';
@ -60,7 +59,7 @@ import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { CoreDatabaseConfiguration, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS'); export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
@ -89,7 +88,7 @@ export class CoreSitesProvider {
// Variables for DB. // Variables for DB.
protected appDB: Promise<SQLiteDB>; protected appDB: Promise<SQLiteDB>;
protected resolveAppDB!: (appDB: SQLiteDB) => void; protected resolveAppDB!: (appDB: SQLiteDB) => void;
protected cacheTables: Record<string, CorePromisedValue<CoreDatabaseTable<CoreSiteWSCacheRecord>>> = {}; protected siteTables: Record<string, Record<string, CorePromisedValue<CoreDatabaseTable>>> = {};
constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) { constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) {
this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
@ -109,15 +108,17 @@ export class CoreSitesProvider {
*/ */
initialize(): void { initialize(): void {
CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => { CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
if (!siteId || !(siteId in this.cacheTables)) { if (!siteId || !(siteId in this.siteTables)) {
return; return;
} }
const cacheTable = await this.cacheTables[siteId]; await Promise.all(
Object
.values(this.siteTables[siteId])
.map(promisedTable => promisedTable.then(table => table.destroy())),
);
delete this.cacheTables[siteId]; delete this.siteTables[siteId];
await cacheTable.destroy();
}); });
} }
@ -135,30 +136,46 @@ export class CoreSitesProvider {
} }
/** /**
* Get cache table. * Get site table.
* *
* @param siteId Site id. * @param tableName Site table name.
* @returns cache table. * @param options Options to configure table initialization.
* @returns Site table.
*/ */
async getCacheTable(site: CoreSite): Promise<CoreDatabaseTable<CoreSiteWSCacheRecord>> { async getSiteTable<
if (!site.id) { DBRecord extends SQLiteDBRecordValues,
throw new CoreError('Can\'t get cache table for site without id'); PrimaryKeyColumn extends keyof DBRecord
>(
tableName: string,
options: Partial<{
siteId: string;
config: Partial<CoreDatabaseConfiguration>;
database: SQLiteDB;
primaryKeyColumns: PrimaryKeyColumn[];
}> = {},
): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> {
const siteId = options.siteId ?? this.getCurrentSiteId();
if (!(siteId in this.siteTables)) {
this.siteTables[siteId] = {};
} }
if (!(site.id in this.cacheTables)) { if (!(tableName in this.siteTables[siteId])) {
const promisedTable = this.cacheTables[site.id] = new CorePromisedValue(); const promisedTable = this.siteTables[siteId][tableName] = new CorePromisedValue();
const table = new CoreDatabaseTableProxy<CoreSiteWSCacheRecord>( const database = options.database ?? await this.getSiteDb(siteId);
{ cachingStrategy: CoreDatabaseCachingStrategy.None }, const table = new CoreDatabaseTableProxy<DBRecord, PrimaryKeyColumn>(
site.getDb(), options.config ?? {},
CoreSite.WS_CACHE_TABLE, database,
tableName,
options.primaryKeyColumns,
); );
await table.initialize(); await table.initialize();
promisedTable.resolve(table); promisedTable.resolve(table as unknown as CoreDatabaseTable);
} }
return this.cacheTables[site.id]; return this.siteTables[siteId][tableName] as unknown as Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>;
} }
/** /**

View File

@ -0,0 +1,40 @@
// (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.
/**
* Lazy map.
*
* Lazy maps are empty by default, but entries are generated lazily when accessed.
*/
export type LazyMap<T> = Record<string, T>;
/**
* Create a map that will initialize entries lazily with the given constructor.
*
* @param lazyConstructor Constructor to use the first time an entry is accessed.
* @returns Lazy map.
*/
export function lazyMap<T>(lazyConstructor: (key: string) => T): LazyMap<T> {
const instances = {};
return new Proxy(instances, {
get(target, property, receiver) {
if (!(property in instances)) {
target[property] = lazyConstructor(property.toString());
}
return Reflect.get(target, property, receiver);
},
});
}