/* 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/. */ const EXPORTED_SYMBOLS = ["CalStorageCachedItemModel"]; const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); const { CalReadableStreamFactory } = ChromeUtils.import( "resource:///modules/CalReadableStreamFactory.jsm" ); const { CalStorageItemModel } = ChromeUtils.import( "resource:///modules/calendar/CalStorageItemModel.jsm" ); /** * CalStorageCachedItemModel extends CalStorageItemModel to add caching support * for items. Most of the methods here are overridden from the parent class to * either add or retrieve items from the cache. */ class CalStorageCachedItemModel extends CalStorageItemModel { /** * Cache for all items. * * @type {Map} */ itemCache = new Map(); /** * Cache for recurring events. * * @type {Map} */ #recurringEventsCache = new Map(); /** * Cache for recurring events offline flags. * * @type {Map} */ #recurringEventsOfflineFlagCache = new Map(); /** * Cache for recurring todos. * * @type {Map} */ #recurringTodosCache = new Map(); /** * Cache for recurring todo offline flags. * * @type {Map} */ #recurringTodosOfflineCache = new Map(); /** * Promise that resolves when the caches have been built up. * * @type {Promise} */ #recurringCachePromise = null; /** * Build up recurring event and todo cache with its offline flags. */ async #ensureRecurringItemCaches() { if (!this.#recurringCachePromise) { this.#recurringCachePromise = this.#buildRecurringItemCaches(); } return this.#recurringCachePromise; } async #buildRecurringItemCaches() { // Retrieve items and flags for recurring events and todos before combining // storing them in the item cache. Items need to be expunged from the // existing item cache to avoid get(Event|Todo)FromRow providing stale // values. let expunge = id => this.itemCache.delete(id); let [events, eventFlags] = await this.getRecurringEventAndFlagMaps(expunge); let [todos, todoFlags] = await this.getRecurringTodoAndFlagMaps(expunge); let itemsMap = await this.getAdditionalDataForItemMap(new Map([...events, ...todos])); this.itemCache = new Map([...this.itemCache, ...itemsMap]); this.#recurringEventsCache = new Map([...this.#recurringEventsCache, ...events]); this.#recurringEventsOfflineFlagCache = new Map([ ...this.#recurringEventsOfflineFlagCache, ...eventFlags, ]); this.#recurringTodosCache = new Map([...this.#recurringTodosCache, ...todos]); this.#recurringTodosOfflineCache = new Map([...this.#recurringTodosOfflineCache, ...todoFlags]); } /** * Overridden here to build the recurring item caches when needed. * * @param {CalStorageQuery} query * * @returns {ReadableStream */ getItems(query) { let self = this; let getStream = () => super.getItems(query); return CalReadableStreamFactory.createReadableStream({ async start(controller) { // HACK because recurring offline events/todos objects don't have offline_journal information // Hence we need to update the offline flags caches. // It can be an expensive operation but is only used in Online Reconciliation mode if ( (query.filters.wantOfflineCreatedItems || query.filters.wantOfflineDeletedItems || query.filters.wantOfflineModifiedItems) && self.mRecItemCachePromise ) { // If there's an existing Promise and it's not complete, wait for it - something else is // already waiting and we don't want to break that by throwing away the caches. If it IS // complete, we'll continue immediately. let recItemCachePromise = self.mRecItemCachePromise; await recItemCachePromise; await new Promise(resolve => ChromeUtils.idleDispatch(resolve)); // Check in case someone else already threw away the caches. if (self.mRecItemCachePromise == recItemCachePromise) { self.mRecItemCachePromise = null; } } await self.#ensureRecurringItemCaches(); for await (let value of cal.iterate.streamValues(getStream())) { controller.enqueue(value); } controller.close(); }, }); } /** * Overridden here to provide the events from the cache. * * @returns {[Map, Map]} */ async getFullRecurringEventAndFlagMaps() { return [this.#recurringEventsCache, this.#recurringEventsOfflineFlagCache]; } /** * Overridden here to provide the todos from the cache. * * @returns {[Map, Map]} */ async getFullRecurringTodoAndFlagMaps() { return [this.#recurringTodosCache, this.#recurringTodosOfflineCache]; } async getEventFromRow(row, getAdditionalData = true) { let item = this.itemCache.get(row.getResultByName("id")); if (item) { return item; } item = await super.getEventFromRow(row, getAdditionalData); if (getAdditionalData) { this.#cacheItem(item); } return item; } async getTodoFromRow(row, getAdditionalData = true) { let item = this.itemCache.get(row.getResultByName("id")); if (item) { return item; } item = await super.getTodoFromRow(row, getAdditionalData); if (getAdditionalData) { this.#cacheItem(item); } return item; } async addItem(item) { await super.addItem(item); this.#cacheItem(item); } async getItemById(id) { await this.#ensureRecurringItemCaches(); let item = this.itemCache.get(id); if (item) { return item; } return super.getItemById(id); } async deleteItemById(id, keepMeta) { await super.deleteItemById(id, keepMeta); this.itemCache.delete(id); this.#recurringEventsCache.delete(id); this.#recurringTodosCache.delete(id); } /** * Adds an item to the relevant caches. * * @param {calIItemBase} item */ #cacheItem(item) { if (item.recurrenceId) { // Do not cache recurring item instances. See bug 1686466. return; } this.itemCache.set(item.id, item); if (item.recurrenceInfo) { if (item.isEvent()) { this.#recurringEventsCache.set(item.id, item); } else { this.#recurringTodosCache.set(item.id, item); } } } }