From 97ae1b3f9d6d4d4551d0ec498262d6a4ebd763b4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 24 Jan 2018 15:06:29 +0100 Subject: [PATCH] MOBILE-2355 sync: Implement sync provider and base class --- src/app/app.module.ts | 4 +- src/classes/base-sync.ts | 201 +++++++++++++++++++++++++++++++++++++++ src/providers/sync.ts | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/classes/base-sync.ts create mode 100644 src/providers/sync.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 83e483cc6..ffe8b4705 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -47,6 +47,7 @@ import { CoreFileSessionProvider } from '../providers/file-session'; import { CoreFilepoolProvider } from '../providers/filepool'; import { CoreUpdateManagerProvider } from '../providers/update-manager'; import { CorePluginFileDelegate } from '../providers/plugin-file-delegate'; +import { CoreSyncProvider } from '../providers/sync'; // Core modules. import { CoreComponentsModule } from '../components/components.module'; @@ -131,7 +132,8 @@ export function createTranslateLoader(http: HttpClient) { CoreFileSessionProvider, CoreFilepoolProvider, CoreUpdateManagerProvider, - CorePluginFileDelegate + CorePluginFileDelegate, + CoreSyncProvider ] }) export class AppModule { diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts new file mode 100644 index 000000000..f87729bb9 --- /dev/null +++ b/src/classes/base-sync.ts @@ -0,0 +1,201 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider } from '../providers/sites'; +import { CoreSyncProvider } from '../providers/sync'; + +/** + * Base class to create sync providers. It provides some common functions. + */ +export class CoreSyncBaseProvider { + /** + * Component of the sync provider. + * @type {string} + */ + component = 'core'; + + /** + * Sync provider's interval. + * @type {number} + */ + syncInterval = 300000; + + // Store sync promises. + protected syncPromises: {[siteId: string]: {[uniqueId: string]: Promise}} = {}; + + constructor(private sitesProvider: CoreSitesProvider) {} + + /** + * Add an ongoing sync to the syncPromises list. On finish the promise will be removed. + * + * @param {number} id Unique sync identifier per component. + * @param {Promise} promise The promise of the sync to add. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} The sync promise. + */ + addOngoingSync(id: number, promise: Promise, siteId?: string) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const uniqueId = this.getUniqueSyncId(id); + if (!this.syncPromises[siteId]) { + this.syncPromises[siteId] = {}; + } + + this.syncPromises[siteId][uniqueId] = promise; + + // Promise will be deleted when finish. + return promise.finally(() => { + delete this.syncPromises[siteId][uniqueId]; + }); + } + + /** + * If there's an ongoing sync for a certain identifier return it. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise of the current sync or undefined if there isn't any. + */ + getOngoingSync(id: number, siteId?: string) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(id, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + const uniqueId = this.getUniqueSyncId(id); + return this.syncPromises[siteId][uniqueId]; + } + } + + /** + * Get the synchronization time. Returns 0 if no time stored. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the time. + */ + getSyncTime(id: number, siteId?: string) : Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.getRecord(CoreSyncProvider.SYNC_TABLE, {component: this.component, id: id}).then((entry) => { + return entry.time; + }).catch(() => { + return 0; + }); + }); + } + + /** + * Get the synchronization warnings of an instance. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the warnings. + */ + getSyncWarnings(id: number, siteId?: string) : Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.getRecord(CoreSyncProvider.SYNC_TABLE, {component: this.component, id: id}).then((entry) => { + try { + return JSON.parse(entry.warnings); + } catch(ex) { + return []; + } + }).catch(() => { + return []; + }); + }); + } + + /** + * Create a unique identifier from component and id. + * + * @param {number} id Unique sync identifier per component. + * @return {string} Unique identifier from component and id. + */ + protected getUniqueSyncId(id: number) : string { + return this.component + '#' + id; + } + + /** + * Check if a there's an ongoing syncronization for the given id. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean} Whether it's synchronizing. + */ + isSyncing(id: number, siteId?: string) : boolean { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const uniqueId = this.getUniqueSyncId(id); + return !!(this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId]); + } + + /** + * Check if a sync is needed: if a certain time has passed since the last time. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether sync is needed. + */ + isSyncNeeded(id: number, siteId?: string) : Promise { + return this.getSyncTime(id, siteId).then((time) => { + return Date.now() - this.syncInterval >= time; + }); + } + + /** + * Set the synchronization time. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [time] Time to set. If not defined, current time. + * @return {Promise} Promise resolved when the time is set. + */ + setSyncTime(id: number, siteId?: string, time?: number) : Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + time = typeof time != 'undefined' ? time : Date.now(); + return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, {time: time}, {component: this.component, id: id}); + }); + } + + /** + * Set the synchronization warnings. + * + * @param {number} id Unique sync identifier per component. + * @param {string[]} warnings Warnings to set. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + setSyncWarnings(id: number, warnings: string[], siteId?: string) : Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + warnings = warnings || []; + return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, {warnings: JSON.stringify(warnings)}, + {component: this.component, id: id}); + }); + } + + /** + * If there's an ongoing sync for a certain identifier, wait for it to end. + * If there's no sync ongoing the promise will be resolved right away. + * + * @param {number} id Unique sync identifier per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when there's no sync going on for the identifier. + */ + waitForSync(id: number, siteId?: string) : Promise { + const promise = this.getOngoingSync(id, siteId); + if (promise) { + return promise.catch(() => {}); + } + return Promise.resolve(); + } +} diff --git a/src/providers/sync.ts b/src/providers/sync.ts new file mode 100644 index 000000000..00b376a11 --- /dev/null +++ b/src/providers/sync.ts @@ -0,0 +1,172 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injectable } from '@angular/core'; +import { CoreEventsProvider } from './events'; +import { CoreSitesProvider } from './sites'; + +/* + * Service that provides some features regarding synchronization. +*/ +@Injectable() +export class CoreSyncProvider { + + // Variables for the database. + public static SYNC_TABLE = 'sync'; + protected tableSchema = { + name: CoreSyncProvider.SYNC_TABLE, + columns: [ + { + name: 'component', + type: 'TEXT', + notNull: true + }, + { + name: 'id', + type: 'INTEGER', + notNull: true + }, + { + name: 'time', + type: 'INTEGER' + }, + { + name: 'warnings', + type: 'TEXT' + } + ], + primaryKeys: ['component', 'id'] + }; + + // Store blocked sync objects. + protected blockedItems: {[siteId: string]: {[blockId: string]: {[operation: string]: boolean}}} = {}; + + constructor(eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider) { + this.sitesProvider.createTableFromSchema(this.tableSchema); + + // Unblock all blocks on logout. + eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => { + this.clearAllBlocks(data.siteId); + }); + } + + /** + * Block a component and ID so it cannot be synchronized. + * + * @param {string} component Component name. + * @param {number} id Unique ID per component. + * @param {string} [operation] Operation name. If not defined, a default text is used. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + blockOperation(component: string, id: number, operation?: string, siteId?: string) : void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const uniqueId = this.getUniqueSyncBlockId(component, id); + + if (!this.blockedItems[siteId]) { + this.blockedItems[siteId] = {}; + } + + if (!this.blockedItems[siteId][uniqueId]) { + this.blockedItems[siteId][uniqueId] = {}; + } + + operation = operation || '-'; + + this.blockedItems[siteId][uniqueId][operation] = true; + } + + /** + * Clear all blocks for a site or all sites. + * + * @param {string} [siteId] If set, clear the blocked objects only for this site. Otherwise clear them for all sites. + */ + clearAllBlocks(siteId?: string) : void { + if (siteId) { + delete this.blockedItems[siteId]; + } else { + this.blockedItems = {}; + } + } + + /** + * Clear all blocks for a certain component. + * + * @param {string} component Component name. + * @param {number} id Unique ID per component. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + clearBlocks(component: string, id: number, siteId?: string) : void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const uniqueId = this.getUniqueSyncBlockId(component, id); + if (this.blockedItems[siteId]) { + delete this.blockedItems[siteId][uniqueId]; + } + } + + /** + * Convenience function to create unique identifiers for a component and id. + * + * @param {string} component Component name. + * @param {number} id Unique ID per component. + * @return {string} Unique sync id. + */ + protected getUniqueSyncBlockId(component: string, id: number) : string { + return component + '#' + id; + } + + /** + * Check if a component is blocked. + * One block can have different operations. Here we check how many operations are being blocking the object. + * + * @param {string} component Component name. + * @param {number} id Unique ID per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean} Whether it's blocked. + */ + isBlocked(component: string, id: number, siteId?: string) : boolean { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!this.blockedItems[siteId]) { + return false; + } + + const uniqueId = this.getUniqueSyncBlockId(component, id); + if (!this.blockedItems[siteId][uniqueId]) { + return false; + } + + return Object.keys(this.blockedItems[siteId][uniqueId]).length > 0; + } + + /** + * Unblock an operation on a component and ID. + * + * @param {string} component Component name. + * @param {number} id Unique ID per component. + * @param {string} [operation] Operation name. If not defined, a default text is used. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + unblockOperation(component: string, id: number, operation?: string, siteId?: string) : void { + operation = operation || '-'; + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const uniqueId = this.getUniqueSyncBlockId(component, id); + + if (this.blockedItems[siteId] && this.blockedItems[siteId][uniqueId]) { + delete this.blockedItems[siteId][uniqueId][operation]; + } + } +}