diff --git a/src/addons/mod/scorm/services/scorm-offline.ts b/src/addons/mod/scorm/services/scorm-offline.ts index 11af7f045..343da11c5 100644 --- a/src/addons/mod/scorm/services/scorm-offline.ts +++ b/src/addons/mod/scorm/services/scorm-offline.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreUser } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; @@ -38,6 +37,10 @@ import { AddonModScormUserDataMap, AddonModScormWSSco, } from './scorm'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; +import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Service to handle offline SCORM. @@ -47,8 +50,36 @@ export class AddonModScormOfflineProvider { protected logger: CoreLogger; + protected tracksTables: LazyMap< + AsyncInstance> + >; + + protected attemptsTables: LazyMap< + AsyncInstance> + >; + constructor() { this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider'); + this.tracksTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(TRACKS_TABLE_NAME, { + siteId, + primaryKeyColumns: ['scormid', 'userid', 'attempt', 'scoid', 'element'], + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }), + ), + ); + this.attemptsTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(ATTEMPTS_TABLE_NAME, { + siteId, + primaryKeyColumns: ['scormid', 'userid', 'attempt'], + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }), + ), + ); } /** @@ -76,40 +107,43 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Change attempt number from ${attempt} to ${newAttempt} in SCORM ${scormId}`); - // Update the attempt number. - const db = site.getDb(); - const currentAttemptConditions: AddonModScormOfflineDBCommonData = { - scormid: scormId, - userid: userId, - attempt, - }; - const newAttemptConditions: AddonModScormOfflineDBCommonData = { - scormid: scormId, - userid: userId, - attempt: newAttempt, - }; - const newAttemptData: Partial = { - attempt: newAttempt, - timemodified: CoreTimeUtils.timestamp(), - }; - // Block the SCORM so it can't be synced. CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id); try { - await db.updateRecords(ATTEMPTS_TABLE_NAME, newAttemptData, currentAttemptConditions); + const currentAttemptConditions = { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, attempt], + js: (record: AddonModScormOfflineDBCommonData) => + record.scormid === scormId && + record.userid === userId && + record.attempt === attempt, + }; + + await this.attemptsTables[site.id].updateWhere( + { attempt: newAttempt, timemodified: CoreTimeUtils.timestamp() }, + currentAttemptConditions, + ); try { // Now update the attempt number of all the tracks and mark them as not synced. - const newTrackData: Partial = { - attempt: newAttempt, - synced: 0, - }; - - await db.updateRecords(TRACKS_TABLE_NAME, newTrackData, currentAttemptConditions); + await this.tracksTables[site.id].updateWhere( + { attempt: newAttempt, synced: 0 }, + currentAttemptConditions, + ); } catch (error) { // Failed to update the tracks, restore the old attempt number. - await db.updateRecords(ATTEMPTS_TABLE_NAME, { attempt }, newAttemptConditions); + await this.attemptsTables[site.id].updateWhere( + { attempt }, + { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, newAttempt], + js: (attempt) => + attempt.scormid === scormId && + attempt.userid === userId && + attempt.attempt === newAttempt, + }, + ); throw error; } @@ -148,7 +182,6 @@ export class AddonModScormOfflineProvider { CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id); // Create attempt in DB. - const db = site.getDb(); const entry: AddonModScormAttemptDBRecord = { scormid: scorm.id, userid: userId, @@ -166,7 +199,7 @@ export class AddonModScormOfflineProvider { } try { - await db.insertRecord(ATTEMPTS_TABLE_NAME, entry); + await this.attemptsTables[site.id].insert(entry); // Store all the data in userData. const promises: Promise[] = []; @@ -204,16 +237,15 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`); - const db = site.getDb(); - const conditions: AddonModScormOfflineDBCommonData = { + const conditions = { scormid: scormId, userid: userId, attempt, }; await Promise.all([ - db.deleteRecords(ATTEMPTS_TABLE_NAME, conditions), - db.deleteRecords(TRACKS_TABLE_NAME, conditions), + this.attemptsTables[site.id].delete(conditions), + this.tracksTables[site.id].delete(conditions), ]); } @@ -280,9 +312,9 @@ export class AddonModScormOfflineProvider { * @returns Promise resolved when the offline attempts are retrieved. */ async getAllAttempts(siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - const attempts = await db.getAllRecords(ATTEMPTS_TABLE_NAME); + const attempts = await this.attemptsTables[siteId].getMany(); return attempts.map((attempt) => this.parseAttempt(attempt)); } @@ -300,7 +332,7 @@ export class AddonModScormOfflineProvider { const site = await CoreSites.getSite(siteId); userId = userId || site.getUserId(); - const attemptRecord = await site.getDb().getRecord(ATTEMPTS_TABLE_NAME, { + const attemptRecord = await this.attemptsTables[site.id].getOneByPrimaryKey({ scormid: scormId, userid: userId, attempt, @@ -340,7 +372,7 @@ export class AddonModScormOfflineProvider { const site = await CoreSites.getSite(siteId); userId = userId || site.getUserId(); - const attempts = await site.getDb().getRecords(ATTEMPTS_TABLE_NAME, { + const attempts = await this.attemptsTables[site.id].getMany({ scormid: scormId, userid: userId, }); @@ -428,7 +460,7 @@ export class AddonModScormOfflineProvider { conditions.synced = 1; } - const tracks = await site.getDb().getRecords(TRACKS_TABLE_NAME, conditions); + const tracks = await this.tracksTables[site.id].getMany(conditions); return this.parseTracks(tracks); } @@ -598,7 +630,6 @@ export class AddonModScormOfflineProvider { userId = userId || site.getUserId(); const scoUserData = scoData?.userdata || {}; - const db = site.getDb(); let lessonStatusInserted = false; if (forceCompleted) { @@ -611,7 +642,16 @@ export class AddonModScormOfflineProvider { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { lessonStatusInserted = true; - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed'); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('completed'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } } } @@ -622,81 +662,35 @@ export class AddonModScormOfflineProvider { } try { - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element, + value: value === undefined ? null : JSON.stringify(value), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } catch (error) { if (lessonStatusInserted) { // Rollback previous insert. - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete'); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('incomplete'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } throw error; } } - /** - * Insert a track in the DB. - * - * @param db Site's DB. - * @param userId User ID. - * @param scormId SCORM ID. - * @param scoId SCO ID. - * @param attempt Attempt number. - * @param element Name of the element to insert. - * @param value Value of the element to insert. - * @param synchronous True if insert should NOT return a promise. Please use it only if synchronous is a must. - * @returns Returns a promise if synchronous=false, otherwise returns a boolean. - */ - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value: AddonModScormDataValue | undefined, - synchronous: true, - ): boolean; - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value?: AddonModScormDataValue, - synchronous?: false, - ): Promise; - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value?: AddonModScormDataValue, - synchronous?: boolean, - ): boolean | Promise { - const entry: AddonModScormTrackDBRecord = { - userid: userId, - scormid: scormId, - scoid: scoId, - attempt, - element: element, - value: value === undefined ? null : JSON.stringify(value), - timemodified: CoreTimeUtils.timestamp(), - synced: 0, - }; - - if (synchronous) { - // The insert operation is always asynchronous, always return true. - db.insertRecord(TRACKS_TABLE_NAME, entry); - - return true; - } else { - return db.insertRecord(TRACKS_TABLE_NAME, entry); - } - } - /** * Insert a track in the offline tracks store, returning a synchronous value. * Please use this function only if synchronous is a must. It's recommended to use insertTrack. @@ -730,8 +724,7 @@ export class AddonModScormOfflineProvider { } const scoUserData = scoData?.userdata || {}; - const db = CoreSites.getRequiredCurrentSite().getDb(); - let lessonStatusInserted = false; + const siteId = CoreSites.getRequiredCurrentSite().id; if (forceCompleted) { if (element == 'cmi.core.lesson_status' && value == 'incomplete') { @@ -741,11 +734,16 @@ export class AddonModScormOfflineProvider { } if (element == 'cmi.core.score.raw') { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { - lessonStatusInserted = true; - - if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed', true)) { - return false; - } + this.tracksTables[siteId].syncInsert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('completed'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } } } @@ -755,15 +753,16 @@ export class AddonModScormOfflineProvider { return true; } - if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value, true)) { - // Insert failed. - if (lessonStatusInserted) { - // Rollback previous insert. - this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete', true); - } - - return false; - } + this.tracksTables[siteId].syncInsert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: element, + value: value === undefined ? null : JSON.stringify(value), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); return true; } @@ -784,7 +783,7 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`); - await site.getDb().updateRecords(TRACKS_TABLE_NAME, { synced: 1 }, > { + await this.tracksTables[site.id].update({ synced: 1 }, { scormid: scormId, userid: userId, attempt, @@ -971,10 +970,13 @@ export class AddonModScormOfflineProvider { snapshot: JSON.stringify(this.removeDefaultData(userData)), }; - await site.getDb().updateRecords(ATTEMPTS_TABLE_NAME, newData, > { - scormid: scormId, - userid: userId, - attempt, + await this.attemptsTables[site.id].updateWhere(newData, { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, attempt], + js: (record: AddonModScormOfflineDBCommonData) => + record.scormid === scormId && + record.userid === userId && + record.attempt === attempt, }); } diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index dc93ec1bc..f78d28756 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -258,6 +258,18 @@ export class CoreDatabaseTable< await this.database.insertRecord(this.tableName, record); } + /** + * Insert a new record synchronously. + * + * @param record Database record. + */ + syncInsert(record: DBRecord): void { + // The current database architecture does not support synchronous operations, + // so calling this method will mean that errors will be silenced. Because of that, + // this should only be called if using the asynchronous alternatives is not possible. + this.insert(record); + } + /** * Update records matching the given conditions. *