diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 5ce95a266..4bcdf942e 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -44,6 +44,7 @@ import { CoreLang } from '@services/lang'; import { CoreSites } from '@services/sites'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CoreDatabaseTable } from './database/database-table'; +import { CoreDatabaseCachingStrategy } from './database/database-table-proxy'; /** * QR Code type enumeration. @@ -140,7 +141,11 @@ 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.cacheTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.WS_CACHE_TABLE, { + siteId: this.getId(), + database: this.getDb(), + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + })); this.setInfo(infos); this.calculateOfflineDisabled(); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index e3f5bab52..08bd6f078 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -48,9 +48,10 @@ import { } from '@services/database/filepool'; import { CoreFileHelper } from './file-helper'; import { CoreUrl } from '@singletons/url'; -import { CorePromisedValue } from '@classes/promised-value'; 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. @@ -101,11 +102,20 @@ export class CoreFilepoolProvider { // Variables for DB. protected appDB: Promise; protected resolveAppDB!: (appDB: SQLiteDB) => void; - protected filesTables: Record>> = {}; + protected filesTables: LazyMap>>; constructor() { this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); + this.filesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(FILES_TABLE_NAME, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, + primaryKeyColumns: ['fileId'], + }), + ), + ); } /** @@ -128,11 +138,9 @@ export class CoreFilepoolProvider { return; } - const filesTable = await this.filesTables[siteId]; + await this.filesTables[siteId].destroy(); delete this.filesTables[siteId]; - - await filesTable.destroy(); }); } @@ -149,33 +157,6 @@ export class CoreFilepoolProvider { this.resolveAppDB(CoreApp.getDB()); } - /** - * Get files table. - * - * @param siteId Site id. - * @returns Files table. - */ - async getFilesTable(siteId?: string): Promise> { - 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( - { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, - database, - FILES_TABLE_NAME, - ['fileId'], - ); - - await table.initialize(); - - filesTable.resolve(table); - } - - return this.filesTables[siteId]; - } - /** * Link a file with a component. * @@ -262,9 +243,7 @@ export class CoreFilepoolProvider { ...data, }; - const filesTable = await this.getFilesTable(siteId); - - await filesTable.insert(record); + await this.filesTables[siteId].insert(record); } /** @@ -605,14 +584,13 @@ export class CoreFilepoolProvider { */ async clearFilepool(siteId: string): Promise { const db = await CoreSites.getSiteDb(siteId); - const filesTable = await this.getFilesTable(siteId); // 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(LINKS_TABLE_NAME); await Promise.all([ - filesTable.delete(), + this.filesTables[siteId].delete(), db.deleteRecords(LINKS_TABLE_NAME), ]); @@ -1167,14 +1145,13 @@ export class CoreFilepoolProvider { } const db = await CoreSites.getSiteDb(siteId); - const filesTable = await this.getFilesTable(siteId); const extension = CoreMimetypeUtils.getFileExtension(entry.path); if (!extension) { // Files does not have extension. Invalidate file (stale = true). // Minor problem: file will remain in the filesystem once downloaded again. 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; } @@ -1184,7 +1161,7 @@ export class CoreFilepoolProvider { entry.fileId = CoreMimetypeUtils.removeExtension(fileId); entry.extension = extension; - await filesTable.update(entry, { fileId }); + await this.filesTables[siteId].update(entry, { fileId }); if (entry.fileId == fileId) { // File ID hasn't changed, we're done. 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 { const db = await CoreSites.getSiteDb(siteId); - const filesTable = await this.getFilesTable(siteId); const items = await this.getComponentFiles(db, component, componentId); const files: CoreFilepoolFileEntry[] = []; await Promise.all(items.map(async (item) => { try { - const fileEntry = await filesTable.findByPrimaryKey({ fileId: item.fileId }); + const fileEntry = await this.filesTables[siteId].findByPrimaryKey({ fileId: item.fileId }); if (!fileEntry) { return; @@ -2184,9 +2160,7 @@ export class CoreFilepoolProvider { * @return Resolved with file object from DB on success, rejected otherwise. */ protected async hasFileInPool(siteId: string, fileId: string): Promise { - const filesTable = await this.getFilesTable(siteId); - - return filesTable.findByPrimaryKey({ fileId }); + return this.filesTables[siteId].findByPrimaryKey({ fileId }); } /** @@ -2218,17 +2192,15 @@ export class CoreFilepoolProvider { * @return Resolved on success. */ async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { - const filesTable = await this.getFilesTable(siteId); - onlyUnknown - ? await filesTable.updateWhere( + ? await this.filesTables[siteId].updateWhere( { stale: 1 }, { sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL, 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 fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); - const filesTable = await this.getFilesTable(siteId); - - await filesTable.update({ stale: 1 }, { fileId }); + await this.filesTables[siteId].update({ stale: 1 }, { fileId }); } /** @@ -2269,7 +2239,6 @@ export class CoreFilepoolProvider { onlyUnknown: boolean = true, ): Promise { const db = await CoreSites.getSiteDb(siteId); - const filesTable = await this.getFilesTable(siteId); const items = await this.getComponentFiles(db, component, componentId); if (!items.length) { @@ -2277,6 +2246,8 @@ export class CoreFilepoolProvider { return; } + siteId = siteId ?? CoreSites.getCurrentSiteId(); + const fileIds = items.map((item) => item.fileId); const whereAndParams = db.getInOrEqual(fileIds); @@ -2287,7 +2258,7 @@ export class CoreFilepoolProvider { whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')'; } - await filesTable.updateWhere( + await this.filesTables[siteId].updateWhere( { stale: 1 }, { sql: whereAndParams.sql, @@ -2714,7 +2685,6 @@ export class CoreFilepoolProvider { */ protected async removeFileById(siteId: string, fileId: string): Promise { 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. // Don't use getFilePath to prevent performing 2 DB requests. @@ -2741,7 +2711,7 @@ export class CoreFilepoolProvider { const promises: Promise[] = []; // Remove entry from filepool store. - promises.push(filesTable.delete(conditions)); + promises.push(this.filesTables[siteId].delete(conditions)); // Remove links. promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions)); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 9de9c1998..ee324ae59 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -31,9 +31,8 @@ import { CoreSiteConfig, CoreSitePublicConfigResponse, CoreSiteInfoResponse, - CoreSiteWSCacheRecord, } from '@classes/site'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { CoreSiteError } from '@classes/errors/siteerror'; import { makeSingleton, Translate, Http } from '@singletons'; @@ -60,7 +59,7 @@ import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CorePromisedValue } from '@classes/promised-value'; 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('CORE_SITE_SCHEMAS'); @@ -89,7 +88,7 @@ export class CoreSitesProvider { // Variables for DB. protected appDB: Promise; protected resolveAppDB!: (appDB: SQLiteDB) => void; - protected cacheTables: Record>> = {}; + protected siteTables: Record>> = {}; constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) { this.appDB = new Promise(resolve => this.resolveAppDB = resolve); @@ -109,15 +108,17 @@ export class CoreSitesProvider { */ initialize(): void { CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => { - if (!siteId || !(siteId in this.cacheTables)) { + if (!siteId || !(siteId in this.siteTables)) { 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]; - - await cacheTable.destroy(); + delete this.siteTables[siteId]; }); } @@ -135,30 +136,46 @@ export class CoreSitesProvider { } /** - * Get cache table. + * Get site table. * - * @param siteId Site id. - * @returns cache table. + * @param tableName Site table name. + * @param options Options to configure table initialization. + * @returns Site table. */ - async getCacheTable(site: CoreSite): Promise> { - if (!site.id) { - throw new CoreError('Can\'t get cache table for site without id'); + async getSiteTable< + DBRecord extends SQLiteDBRecordValues, + PrimaryKeyColumn extends keyof DBRecord + >( + tableName: string, + options: Partial<{ + siteId: string; + config: Partial; + database: SQLiteDB; + primaryKeyColumns: PrimaryKeyColumn[]; + }> = {}, + ): Promise> { + const siteId = options.siteId ?? this.getCurrentSiteId(); + + if (!(siteId in this.siteTables)) { + this.siteTables[siteId] = {}; } - if (!(site.id in this.cacheTables)) { - const promisedTable = this.cacheTables[site.id] = new CorePromisedValue(); - const table = new CoreDatabaseTableProxy( - { cachingStrategy: CoreDatabaseCachingStrategy.None }, - site.getDb(), - CoreSite.WS_CACHE_TABLE, + if (!(tableName in this.siteTables[siteId])) { + const promisedTable = this.siteTables[siteId][tableName] = new CorePromisedValue(); + const database = options.database ?? await this.getSiteDb(siteId); + const table = new CoreDatabaseTableProxy( + options.config ?? {}, + database, + tableName, + options.primaryKeyColumns, ); 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>; } /** diff --git a/src/core/utils/lazy-map.ts b/src/core/utils/lazy-map.ts new file mode 100644 index 000000000..3570c111c --- /dev/null +++ b/src/core/utils/lazy-map.ts @@ -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 = Record; + +/** + * 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(lazyConstructor: (key: string) => T): LazyMap { + 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); + }, + }); +}