diff options
Diffstat (limited to 'comm/calendar/base/src/calCachedCalendar.js')
-rw-r--r-- | comm/calendar/base/src/calCachedCalendar.js | 957 |
1 files changed, 957 insertions, 0 deletions
diff --git a/comm/calendar/base/src/calCachedCalendar.js b/comm/calendar/base/src/calCachedCalendar.js new file mode 100644 index 0000000000..3dd2d872a4 --- /dev/null +++ b/comm/calendar/base/src/calCachedCalendar.js @@ -0,0 +1,957 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["calCachedCalendar"]; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var calICalendar = Ci.calICalendar; +var cICL = Ci.calIChangeLog; +var cIOL = Ci.calIOperationListener; + +var gNoOpListener = { + QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]), + onGetResult(calendar, status, itemType, detail, items) {}, + + onOperationComplete(calendar, status, opType, id, detail) {}, +}; + +/** + * Returns true if the exception passed is one that should cause the cache + * layer to retry the operation. This is usually a network error or other + * temporary error. + * + * @param result The result code to check. + * @returns True, if the result code means server unavailability. + */ +function isUnavailableCode(result) { + // Stolen from nserror.h + const NS_ERROR_MODULE_NETWORK = 6; + function NS_ERROR_GET_MODULE(code) { + return ((code >> 16) - 0x45) & 0x1fff; + } + + if (NS_ERROR_GET_MODULE(result) == NS_ERROR_MODULE_NETWORK && !Components.isSuccessCode(result)) { + // This is a network error, which most likely means we should + // retry it some time. + return true; + } + + // Other potential errors we want to retry with + switch (result) { + case Cr.NS_ERROR_NOT_AVAILABLE: + return true; + default: + return false; + } +} + +function calCachedCalendarObserverHelper(home, isCachedObserver) { + this.home = home; + this.isCachedObserver = isCachedObserver; +} +calCachedCalendarObserverHelper.prototype = { + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + isCachedObserver: false, + + onStartBatch() { + this.home.mObservers.notify("onStartBatch", [this.home]); + }, + + onEndBatch() { + this.home.mObservers.notify("onEndBatch", [this.home]); + }, + + async onLoad(calendar) { + if (this.isCachedObserver) { + this.home.mObservers.notify("onLoad", [this.home]); + } else { + // start sync action after uncached calendar has been loaded. + // xxx todo, think about: + // although onAddItem et al have been called, we need to fire + // an additional onLoad completing the refresh call (->composite) + let home = this.home; + await home.synchronize(); + home.mObservers.notify("onLoad", [home]); + } + }, + + onAddItem(aItem) { + if (this.isCachedObserver) { + this.home.mObservers.notify("onAddItem", arguments); + } + }, + + onModifyItem(aNewItem, aOldItem) { + if (this.isCachedObserver) { + this.home.mObservers.notify("onModifyItem", arguments); + } + }, + + onDeleteItem(aItem) { + if (this.isCachedObserver) { + this.home.mObservers.notify("onDeleteItem", arguments); + } + }, + + onError(aCalendar, aErrNo, aMessage) { + this.home.mObservers.notify("onError", arguments); + }, + + onPropertyChanged(aCalendar, aName, aValue, aOldValue) { + if (!this.isCachedObserver) { + this.home.mObservers.notify("onPropertyChanged", [this.home, aName, aValue, aOldValue]); + } + }, + + onPropertyDeleting(aCalendar, aName) { + if (!this.isCachedObserver) { + this.home.mObservers.notify("onPropertyDeleting", [this.home, aName]); + } + }, +}; + +function calCachedCalendar(uncachedCalendar) { + this.wrappedJSObject = this; + this.mSyncQueue = []; + this.mObservers = new cal.data.ObserverSet(Ci.calIObserver); + uncachedCalendar.superCalendar = this; + uncachedCalendar.addObserver(new calCachedCalendarObserverHelper(this, false)); + this.mUncachedCalendar = uncachedCalendar; + this.setupCachedCalendar(); + if (this.supportsChangeLog) { + uncachedCalendar.offlineStorage = this.mCachedCalendar; + } + this.offlineCachedItems = {}; + this.offlineCachedItemFlags = {}; +} +calCachedCalendar.prototype = { + /* eslint-disable mozilla/use-chromeutils-generateqi */ + QueryInterface(aIID) { + if (aIID.equals(Ci.calISchedulingSupport) && this.mUncachedCalendar.QueryInterface(aIID)) { + // check whether uncached calendar supports it: + return this; + } else if (aIID.equals(Ci.calICalendar) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + /* eslint-enable mozilla/use-chromeutils-generateqi */ + + mCachedCalendar: null, + mCachedObserver: null, + mUncachedCalendar: null, + mObservers: null, + mSuperCalendar: null, + offlineCachedItems: null, + offlineCachedItemFlags: null, + + onCalendarUnregistering() { + if (this.mCachedCalendar) { + let self = this; + this.mCachedCalendar.removeObserver(this.mCachedObserver); + // TODO put changes into a different calendar and delete + // afterwards. + + let listener = { + onDeleteCalendar(aCalendar, aStatus, aDetail) { + self.mCachedCalendar = null; + }, + }; + + this.mCachedCalendar + .QueryInterface(Ci.calICalendarProvider) + .deleteCalendar(this.mCachedCalendar, listener); + } + }, + + setupCachedCalendar() { + try { + if (this.mCachedCalendar) { + // this is actually a resetupCachedCalendar: + // Although this doesn't really follow the spec, we know the + // storage calendar's deleteCalendar method is synchronous. + // TODO put changes into a different calendar and delete + // afterwards. + this.mCachedCalendar + .QueryInterface(Ci.calICalendarProvider) + .deleteCalendar(this.mCachedCalendar, null); + if (this.supportsChangeLog) { + // start with full sync: + this.mUncachedCalendar.resetLog(); + } + } else { + let calType = Services.prefs.getStringPref("calendar.cache.type", "storage"); + // While technically, the above deleteCalendar should delete the + // whole calendar, this is nothing more than deleting all events + // todos and properties. Therefore the initialization can be + // skipped. + let cachedCalendar = Cc["@mozilla.org/calendar/calendar;1?type=" + calType].createInstance( + Ci.calICalendar + ); + switch (calType) { + case "memory": { + if (this.supportsChangeLog) { + // start with full sync: + this.mUncachedCalendar.resetLog(); + } + break; + } + case "storage": { + let file = cal.provider.getCalendarDirectory(); + file.append("cache.sqlite"); + cachedCalendar.uri = Services.io.newFileURI(file); + cachedCalendar.id = this.id; + break; + } + default: { + throw new Error("unsupported cache calendar type: " + calType); + } + } + cachedCalendar.transientProperties = true; + // Forward the disabled property to the storage calendar so that it + // stops interacting with the file system. Other properties have no + // useful effect on the storage calendar, so don't forward them. + cachedCalendar.setProperty("disabled", this.getProperty("disabled")); + cachedCalendar.setProperty("relaxedMode", true); + cachedCalendar.superCalendar = this; + if (!this.mCachedObserver) { + this.mCachedObserver = new calCachedCalendarObserverHelper(this, true); + } + cachedCalendar.addObserver(this.mCachedObserver); + this.mCachedCalendar = cachedCalendar; + } + } catch (exc) { + console.error(exc); + } + }, + + async getOfflineAddedItems() { + this.offlineCachedItems = {}; + for await (let items of cal.iterate.streamValues( + this.mCachedCalendar.getItems( + calICalendar.ITEM_FILTER_ALL_ITEMS | calICalendar.ITEM_FILTER_OFFLINE_CREATED, + 0, + null, + null + ) + )) { + for (let item of items) { + this.offlineCachedItems[item.hashId] = item; + this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_CREATED_RECORD; + } + } + }, + + async getOfflineModifiedItems() { + for await (let items of cal.iterate.streamValues( + this.mCachedCalendar.getItems( + calICalendar.ITEM_FILTER_OFFLINE_MODIFIED | calICalendar.ITEM_FILTER_ALL_ITEMS, + 0, + null, + null + ) + )) { + for (let item of items) { + this.offlineCachedItems[item.hashId] = item; + this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_MODIFIED_RECORD; + } + } + }, + + async getOfflineDeletedItems() { + for await (let items of cal.iterate.streamValues( + this.mCachedCalendar.getItems( + calICalendar.ITEM_FILTER_OFFLINE_DELETED | calICalendar.ITEM_FILTER_ALL_ITEMS, + 0, + null, + null + ) + )) { + for (let item of items) { + this.offlineCachedItems[item.hashId] = item; + this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_DELETED_RECORD; + } + } + }, + + mPendingSync: null, + async synchronize() { + if (!this.mPendingSync) { + this.mPendingSync = this._doSynchronize().catch(console.error); + } + return this.mPendingSync; + }, + async _doSynchronize() { + let clearPending = () => { + this.mPendingSync = null; + }; + + if (this.getProperty("disabled")) { + clearPending(); + return; + } + + if (this.offline) { + clearPending(); + return; + } + + if (this.supportsChangeLog) { + await new Promise((resolve, reject) => { + let spec = this.uri.spec; + cal.LOG("[calCachedCalendar] Doing changelog based sync for calendar " + spec); + let opListener = { + onResult(operation, result) { + if (!operation || !operation.isPending) { + let status = operation ? operation.status : Cr.NS_OK; + clearPending(); + if (!Components.isSuccessCode(status)) { + reject( + "[calCachedCalendar] replay action failed: " + + (operation && operation.id ? operation.id : "<unknown>") + + ", uri=" + + spec + + ", result=" + + result + + ", operation=" + + operation + ); + return; + } + cal.LOG("[calCachedCalendar] replayChangesOn finished."); + resolve(); + } + }, + }; + this.mUncachedCalendar.replayChangesOn(opListener); + }); + return; + } + + cal.LOG("[calCachedCalendar] Doing full sync for calendar " + this.uri.spec); + + await this.getOfflineAddedItems(); + await this.getOfflineModifiedItems(); + await this.getOfflineDeletedItems(); + + // TODO instead of deleting the calendar and creating a new + // one, maybe we want to do a "real" sync between the + // existing local calendar and the remote calendar. + this.setupCachedCalendar(); + + let modifiedTimes = {}; + try { + for await (let items of cal.iterate.streamValues( + this.mUncachedCalendar.getItems(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null) + )) { + for (let item of items) { + // Adding items recd from the Memory Calendar + // These may be different than what the cache has + modifiedTimes[item.id] = item.lastModifiedTime; + this.mCachedCalendar.addItem(item); + } + } + } catch (e) { + await this.playbackOfflineItems(); + this.mCachedObserver.onLoad(this.mCachedCalendar); + clearPending(); + throw e; // Do not swallow this error. + } + + await new Promise((resolve, reject) => { + cal.iterate.forEach( + this.offlineCachedItems, + item => { + switch (this.offlineCachedItemFlags[item.hashId]) { + case cICL.OFFLINE_FLAG_CREATED_RECORD: + // Created items are not present on the server, so its safe to adopt them + this.adoptOfflineItem(item.clone()); + break; + case cICL.OFFLINE_FLAG_MODIFIED_RECORD: + // Two Cases Here: + if (item.id in modifiedTimes) { + // The item is still on the server, we just retrieved it in the listener above. + if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) { + // The item on the server has been modified, ask to overwrite + cal.WARN( + "[calCachedCalendar] Item '" + + item.title + + "' at the server seems to be modified recently." + ); + this.promptOverwrite("modify", item, null); + } else { + // Our item is newer, just modify the item + this.modifyOfflineItem(item, null); + } + } else { + // The item has been deleted from the server, ask if it should be added again + cal.WARN( + "[calCachedCalendar] Item '" + item.title + "' has been deleted from the server" + ); + if (cal.provider.promptOverwrite("modify", item, null)) { + this.adoptOfflineItem(item.clone()); + } + } + break; + case cICL.OFFLINE_FLAG_DELETED_RECORD: + if (item.id in modifiedTimes) { + // The item seems to exist on the server... + if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) { + // ...and has been modified on the server. Ask to overwrite + cal.WARN( + "[calCachedCalendar] Item '" + + item.title + + "' at the server seems to be modified recently." + ); + this.promptOverwrite("delete", item, null); + } else { + // ...and has not been modified. Delete it now. + this.deleteOfflineItem(item); + } + } else { + // Item has already been deleted from the server, no need to change anything. + } + break; + } + }, + async () => { + this.offlineCachedItems = {}; + this.offlineCachedItemFlags = {}; + await this.playbackOfflineItems(); + clearPending(); + resolve(); + } + ); + }); + }, + + onOfflineStatusChanged(aNewState) { + if (aNewState) { + // Going offline: (XXX get items before going offline?) => we may ask the user to stay online a bit longer + } else if (!this.getProperty("disabled") && this.getProperty("refreshInterval") != "0") { + // Going online (start replaying changes to the remote calendar). + // Don't do this if the calendar is disabled or set to manual updates only. + this.refresh(); + } + }, + + // aOldItem is already in the cache + async promptOverwrite(aMethod, aItem, aOldItem) { + let overwrite = cal.provider.promptOverwrite(aMethod, aItem); + if (overwrite) { + if (aMethod == "modify") { + await this.modifyOfflineItem(aItem, aOldItem); + } else { + await this.deleteOfflineItem(aItem); + } + } + }, + + /* + * Asynchronously performs playback operations of items added, modified, or deleted offline + * + * @param aPlaybackType (optional) The starting operation type. This function will be + * called recursively through playback operations in the order of + * add, modify, delete. By default playback will start with the add + * operation. Valid values for this parameter are defined as + * OFFLINE_FLAG_XXX constants in the calIChangeLog interface. + */ + async playbackOfflineItems(aPlaybackType) { + let self = this; + let storage = this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage); + + let itemQueue = []; + let debugOp; + let nextCallback; + let uncachedOp; + let filter; + + aPlaybackType = aPlaybackType || cICL.OFFLINE_FLAG_CREATED_RECORD; + switch (aPlaybackType) { + case cICL.OFFLINE_FLAG_CREATED_RECORD: + debugOp = "add"; + nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_MODIFIED_RECORD); + uncachedOp = item => this.mUncachedCalendar.addItem(item); + filter = calICalendar.ITEM_FILTER_OFFLINE_CREATED; + break; + case cICL.OFFLINE_FLAG_MODIFIED_RECORD: + debugOp = "modify"; + nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_DELETED_RECORD); + uncachedOp = item => this.mUncachedCalendar.modifyItem(item, item); + filter = calICalendar.ITEM_FILTER_OFFLINE_MODIFIED; + break; + case cICL.OFFLINE_FLAG_DELETED_RECORD: + debugOp = "delete"; + uncachedOp = item => this.mUncachedCalendar.deleteItem(item); + filter = calICalendar.ITEM_FILTER_OFFLINE_DELETED; + break; + default: + cal.ERROR("[calCachedCalendar] Invalid playback type: " + aPlaybackType); + return; + } + + async function popItemQueue() { + if (!itemQueue || itemQueue.length == 0) { + // no items left in the queue, move on to the next operation + if (nextCallback) { + await nextCallback(); + } + } else { + // perform operation on the next offline item in the queue + let item = itemQueue.pop(); + let error = null; + try { + await uncachedOp(item); + } catch (e) { + error = e; + cal.ERROR( + "[calCachedCalendar] Could not perform playback operation " + + debugOp + + " for item " + + (item.title || " (none) ") + + ": " + + e + ); + } + if (!error) { + if (aPlaybackType == cICL.OFFLINE_FLAG_DELETED_RECORD) { + self.mCachedCalendar.deleteItem(item); + } else { + storage.resetItemOfflineFlag(item); + } + } else { + // If the playback action could not be performed, then there + // is no need for further action. The item still has the + // offline flag, so it will be taken care of next time. + cal.WARN( + "[calCachedCalendar] Unable to perform playback action " + + debugOp + + " to the server, will try again next time (" + + item.id + + "," + + error + + ")" + ); + } + + // move on to the next item in the queue + await popItemQueue(); + } + } + + itemQueue = itemQueue.concat( + await this.mCachedCalendar.getItemsAsArray( + calICalendar.ITEM_FILTER_ALL_ITEMS | filter, + 0, + null, + null + ) + ); + + if (this.offline) { + cal.LOG("[calCachedCalendar] back to offline mode, reconciliation aborted"); + } else { + cal.LOG( + "[calCachedCalendar] Performing playback operation " + + debugOp + + " on " + + itemQueue.length + + " items to " + + self.name + ); + // start the first operation + await popItemQueue(); + } + }, + + get superCalendar() { + return (this.mSuperCalendar && this.mSuperCalendar.superCalendar) || this; + }, + set superCalendar(val) { + this.mSuperCalendar = val; + }, + + get offline() { + return Services.io.offline; + }, + get supportsChangeLog() { + return cal.wrapInstance(this.mUncachedCalendar, Ci.calIChangeLog) != null; + }, + + get canRefresh() { + // enable triggering sync using the reload button + return true; + }, + + get supportsScheduling() { + return this.mUncachedCalendar.supportsScheduling; + }, + + getSchedulingSupport() { + return this.mUncachedCalendar.getSchedulingSupport(); + }, + + getProperty(aName) { + switch (aName) { + case "cache.enabled": + if (this.mUncachedCalendar.getProperty("cache.always")) { + return true; + } + break; + } + + return this.mUncachedCalendar.getProperty(aName); + }, + setProperty(aName, aValue) { + if (aName == "disabled") { + // Forward the disabled property to the storage calendar so that it + // stops interacting with the file system. Other properties have no + // useful effect on the storage calendar, so don't forward them. + this.mCachedCalendar.setProperty(aName, aValue); + } + this.mUncachedCalendar.setProperty(aName, aValue); + }, + async refresh() { + if (this.offline) { + this.downstreamRefresh(); + } else if (this.supportsChangeLog) { + /* we first ensure that any remaining offline items are reconciled with the calendar server */ + await this.playbackOfflineItems(); + await this.downstreamRefresh(); + } else { + this.downstreamRefresh(); + } + }, + async downstreamRefresh() { + if (this.mUncachedCalendar.canRefresh && !this.offline) { + this.mUncachedCalendar.refresh(); // will trigger synchronize once the calendar is loaded + return; + } + await this.synchronize(); + // fire completing onLoad for this refresh call + this.mCachedObserver.onLoad(this.mCachedCalendar); + }, + + addObserver(aObserver) { + this.mObservers.add(aObserver); + }, + removeObserver(aObserver) { + this.mObservers.delete(aObserver); + }, + + async addItem(item) { + return this.adoptItem(item.clone()); + }, + + async adoptItem(item) { + return new Promise((resolve, reject) => { + this.doAdoptItem(item, (calendar, status, opType, id, detail) => { + if (!Components.isSuccessCode(status)) { + return reject(new Components.Exception(detail, status)); + } + return resolve(detail); + }); + }); + }, + + /** + * The function form of calIOperationListener.onOperationComplete used where + * the whole interface is not needed. + * + * @callback OnOperationCompleteHandler + * + * @param {calICalendar} calendar + * @param {number} status + * @param {number} operationType + * @param {string} id + * @param {calIItem|Error} detail + */ + + /** + * Keeps track of pending callbacks injected into the uncached calendar during + * adopt or modify operations. This is done to ensure we remove the correct + * callback when multiple operations occur at once. + * + * @type {OnOperationComplateHandler[]} + */ + _injectedCallbacks: [], + + /** + * Executes the actual addition of the item using either the cached or uncached + * calendar depending on offline state. A separate method is used here to + * preserve the order of the "onAddItem" event. + * + * @param {calIItem} item + * @param {OnOperationCompleteHandler} handler + */ + doAdoptItem(item, listener) { + // Forwarding add/modify/delete to the cached calendar using the calIObserver + // callbacks would be advantageous, because the uncached provider could implement + // a true push mechanism firing without being triggered from within the program. + // But this would mean the uncached provider fires on the passed + // calIOperationListener, e.g. *before* it fires on calIObservers + // (because that order is undefined). Firing onOperationComplete before onAddItem et al + // would result in this facade firing onOperationComplete even though the modification + // hasn't yet been performed on the cached calendar (which happens in onAddItem et al). + // Result is that we currently stick to firing onOperationComplete if the cached calendar + // has performed the modification, see below: + + let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.ADD, item.id, item); + let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); + + if (this.offline) { + // If we are offline, don't even try to add the item + this.adoptOfflineItem(item).then(onSuccess, onError); + } else { + // Otherwise ask the provider to add the item now. + + // Expected to be called in the context of the uncached calendar's adoptItem() + // so this adoptItem() call returns first. This is a needed hack to keep the + // cached calendar's "onAddItem" event firing before the endBatch() call of + // the uncached calendar. + let adoptItemCallback = async (calendar, status, opType, id, detail) => { + if (isUnavailableCode(status)) { + // The item couldn't be added to the (remote) location, + // this is like being offline. Add the item to the cached + // calendar instead. + cal.LOG( + "[calCachedCalendar] Calendar " + calendar.name + " is unavailable, adding item offline" + ); + await this.adoptOfflineItem(item).then(onSuccess, onError); + } else if (Components.isSuccessCode(status)) { + // On success, add the item to the cache. + await this.mCachedCalendar.addItem(detail).then(onSuccess, onError); + } else { + // Either an error occurred or this is a successful add + // to a cached calendar. Forward the call to the listener + listener(this, status, opType, id, detail); + } + this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = null; + this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != adoptItemCallback); + }; + + // Store the callback so we can remove the correct one later. + this._injectedCallbacks.push(adoptItemCallback); + + this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = adoptItemCallback; + this.mUncachedCalendar.adoptItem(item).catch(e => { + adoptItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); + }); + } + }, + + /** + * Adds an item to the cached (storage) calendar. + * + * @param {calIItem} item + * @returns {calIItem} + */ + async adoptOfflineItem(item) { + let adoptedItem = await this.mCachedCalendar.adoptItem(item); + await this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).addOfflineItem(adoptedItem); + return adoptedItem; + }, + + async modifyItem(newItem, oldItem) { + return new Promise((resolve, reject) => { + this.doModifyItem(newItem, oldItem, (calendar, status, opType, id, detail) => { + if (!Components.isSuccessCode(status)) { + return reject(new Components.Exception(detail, status)); + } + return resolve(detail); + }); + }); + }, + + /** + * Executes the actual modification of the item using either the cached or + * uncached calendar depending on offline state. A separate method is used here + * to preserve the order of the "onModifyItem" event. + * + * @param {calIItem} newItem + * @param {calIItem} oldItem + * @param {OnOperationCompleteHandler} handler + */ + doModifyItem(newItem, oldItem, listener) { + let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.MODIFY, item.id, item); + let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); + + // Forwarding add/modify/delete to the cached calendar using the calIObserver + // callbacks would be advantageous, because the uncached provider could implement + // a true push mechanism firing without being triggered from within the program. + // But this would mean the uncached provider fires on the passed + // calIOperationListener, e.g. *before* it fires on calIObservers + // (because that order is undefined). Firing onOperationComplete before onAddItem et al + // would result in this facade firing onOperationComplete even though the modification + // hasn't yet been performed on the cached calendar (which happens in onAddItem et al). + // Result is that we currently stick to firing onOperationComplete if the cached calendar + // has performed the modification, see below: */ + + // Expected to be called in the context of the uncached calendar's modifyItem() + // so this modifyItem() call returns first. This is a needed hack to keep the + // cached calendar's "onModifyItem" event firing before the endBatch() call of + // the uncached calendar. + let modifyItemCallback = async (calendar, status, opType, id, detail) => { + // Returned Promise only available through wrappedJSObject. + if (isUnavailableCode(status)) { + // The item couldn't be modified at the (remote) location, + // this is like being offline. Add the item to the cache + // instead. + cal.LOG( + "[calCachedCalendar] Calendar " + + calendar.name + + " is unavailable, modifying item offline" + ); + await this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); + } else if (Components.isSuccessCode(status)) { + // On success, modify the item in the cache + await this.mCachedCalendar.modifyItem(detail, oldItem).then(onSuccess, onError); + } else { + // This happens on error, forward the error through the listener + listener(this, status, opType, id, detail); + } + this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != modifyItemCallback); + }; + + // First of all, we should find out if the item to modify is + // already an offline item or not. + if (this.offline) { + // If we are offline, don't even try to modify the item + this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); + } else { + // Otherwise, get the item flags and further process the item. + this.mCachedCalendar.getItemOfflineFlag(oldItem).then(offline_flag => { + if ( + offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD || + offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD + ) { + // The item is already offline, just modify it in the cache + this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); + } else { + // Not an offline item, attempt to modify using provider + + // This is a needed hack to keep the cached calendar's "onModifyItem" event + // firing before the endBatch() call of the uncached calendar. It is called + // in mUncachedCalendar's modifyItem() method. + this.mUncachedCalendar.wrappedJSObject._cachedModifyItemCallback = modifyItemCallback; + + // Store the callback so we can remove the correct one later. + this._injectedCallbacks.push(modifyItemCallback); + + this.mUncachedCalendar.modifyItem(newItem, oldItem).catch(e => { + modifyItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); + }); + } + }); + } + }, + + /** + * Modifies an item in the cached calendar. + * + * @param {calIItem} newItem + * @param {calIItem} oldItem + * @returns {calIItem} + */ + async modifyOfflineItem(newItem, oldItem) { + let modifiedItem = await this.mCachedCalendar.modifyItem(newItem, oldItem); + await this.mCachedCalendar + .QueryInterface(Ci.calIOfflineStorage) + .modifyOfflineItem(modifiedItem); + return modifiedItem; + }, + + async deleteItem(item) { + // First of all, we should find out if the item to delete is + // already an offline item or not. + if (this.offline) { + // If we are offline, don't even try to delete the item + await this.deleteOfflineItem(item); + } else { + // Otherwise, get the item flags, the listener will further + // process the item. + let offline_flag = await this.mCachedCalendar.getItemOfflineFlag(item); + if ( + offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD || + offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD + ) { + // The item is already offline, just mark it deleted it in + // the cache + await this.deleteOfflineItem(item); + } else { + try { + // Not an offline item, attempt to delete using provider + await this.mUncachedCalendar.deleteItem(item); + + // On success, delete the item from the cache + await this.mCachedCalendar.deleteItem(item); + + // Also, remove any meta data associated with the item + try { + this.mCachedCalendar.QueryInterface(Ci.calISyncWriteCalendar).deleteMetaData(item.id); + } catch (e) { + cal.LOG("[calCachedCalendar] Offline storage doesn't support metadata"); + } + } catch (e) { + if (isUnavailableCode(e.result)) { + // The item couldn't be deleted at the (remote) location, + // this is like being offline. Mark the item deleted in the + // cache instead. + cal.LOG( + "[calCachedCalendar] Calendar " + + item.calendar.name + + " is unavailable, deleting item offline" + ); + await this.deleteOfflineItem(item); + } + } + } + } + }, + + async deleteOfflineItem(item) { + /* We do not delete the item from the cache, as we will need it when reconciling the cache content and the server content. */ + return this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).deleteOfflineItem(item); + }, +}; +(function () { + function defineForwards(proto, targetName, functions, getters, gettersAndSetters) { + function defineForwardGetter(attr) { + proto.__defineGetter__(attr, function () { + return this[targetName][attr]; + }); + } + function defineForwardGetterAndSetter(attr) { + defineForwardGetter(attr); + proto.__defineSetter__(attr, function (value) { + return (this[targetName][attr] = value); + }); + } + function defineForwardFunction(funcName) { + proto[funcName] = function (...args) { + let obj = this[targetName]; + return obj[funcName](...args); + }; + } + functions.forEach(defineForwardFunction); + getters.forEach(defineForwardGetter); + gettersAndSetters.forEach(defineForwardGetterAndSetter); + } + + defineForwards( + calCachedCalendar.prototype, + "mUncachedCalendar", + ["deleteProperty", "isInvitation", "getInvitedAttendee", "canNotify"], + ["providerID", "type", "aclManager", "aclEntry"], + ["id", "name", "uri", "readOnly"] + ); + defineForwards( + calCachedCalendar.prototype, + "mCachedCalendar", + ["getItem", "getItems", "getItemsAsArray", "startBatch", "endBatch"], + [], + [] + ); +})(); |