| /** |
| * @license |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * 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 {StorageLocation, StorageObject, StorageService} from './gr-storage'; |
| import {Finalizable} from '../registry'; |
| import {NumericChangeId} from '../../types/common'; |
| |
| export const DURATION_DAY = 24 * 60 * 60 * 1000; |
| |
| // Clean up old entries no more frequently than one day. |
| const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY; |
| |
| const CLEANUP_PREFIXES_MAX_AGE_MAP = new Map<string, number>(); |
| CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY); |
| CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY); |
| |
| export class GrStorageService implements StorageService, Finalizable { |
| private lastCleanup = 0; |
| |
| private readonly storage = window.localStorage; |
| |
| private exceededQuota = false; |
| |
| finalize() {} |
| |
| getDraftComment(location: StorageLocation): StorageObject | null { |
| this.cleanupItems(); |
| return this.getObject(this.getDraftKey(location)); |
| } |
| |
| setDraftComment(location: StorageLocation, message: string) { |
| const key = this.getDraftKey(location); |
| this.setObject(key, {message, updated: Date.now()}); |
| } |
| |
| eraseDraftComment(location: StorageLocation) { |
| const key = this.getDraftKey(location); |
| this.storage.removeItem(key); |
| } |
| |
| getEditableContentItem(key: string): StorageObject | null { |
| this.cleanupItems(); |
| return this.getObject(this.getEditableContentKey(key)); |
| } |
| |
| setEditableContentItem(key: string, message: string) { |
| this.setObject(this.getEditableContentKey(key), { |
| message, |
| updated: Date.now(), |
| }); |
| } |
| |
| eraseEditableContentItem(key: string) { |
| this.storage.removeItem(this.getEditableContentKey(key)); |
| } |
| |
| /** |
| * Deletes all keys for cached edits. |
| * |
| * @param changeNum |
| */ |
| eraseEditableContentItemsForChangeEdit(changeNum?: NumericChangeId) { |
| if (!changeNum) return; |
| |
| // Fetch all keys and then match them up to the keys we want. |
| for (const key of Object.keys(this.storage)) { |
| // Only delete the value that starts with editablecontent:c${changeNum}_ps |
| // to prevent deleting unrelated keys. |
| if (key.startsWith(`editablecontent:c${changeNum}_ps`)) { |
| // We have to remove editablecontent: from the string as it is |
| // automatically added to the string within the storage. |
| this.eraseEditableContentItem(key.replace('editablecontent:', '')); |
| } |
| } |
| } |
| |
| private getDraftKey(location: StorageLocation): string { |
| const range = location.range |
| ? `${location.range.start_line}-${location.range.start_character}` + |
| `-${location.range.end_character}-${location.range.end_line}` |
| : null; |
| let key = [ |
| 'draft', |
| location.changeNum, |
| location.patchNum, |
| location.path, |
| location.line || '', |
| ].join(':'); |
| if (range) { |
| key = key + ':' + range; |
| } |
| return key; |
| } |
| |
| private getEditableContentKey(key: string): string { |
| return `editablecontent:${key}`; |
| } |
| |
| private cleanupItems() { |
| // Throttle cleanup to the throttle interval. |
| if ( |
| this.lastCleanup && |
| Date.now() - this.lastCleanup < CLEANUP_THROTTLE_INTERVAL |
| ) { |
| return; |
| } |
| this.lastCleanup = Date.now(); |
| |
| Object.keys(this.storage).forEach(key => { |
| const entries = CLEANUP_PREFIXES_MAX_AGE_MAP.entries(); |
| for (const [prefix, expiration] of entries) { |
| if (key.startsWith(prefix)) { |
| const item = this.getObject(key); |
| if (!item || Date.now() - item.updated > expiration) { |
| this.storage.removeItem(key); |
| } |
| } |
| } |
| }); |
| } |
| |
| private getObject(key: string): StorageObject | null { |
| const serial = this.storage.getItem(key); |
| if (!serial) { |
| return null; |
| } |
| return JSON.parse(serial) as StorageObject; |
| } |
| |
| private setObject(key: string, obj: StorageObject) { |
| if (this.exceededQuota) { |
| return; |
| } |
| try { |
| this.storage.setItem(key, JSON.stringify(obj)); |
| } catch (exc: unknown) { |
| if (exc instanceof DOMException) { |
| // Catch for QuotaExceededError and disable writes on local storage the |
| // first time that it occurs. |
| if (exc.code === 22) { |
| this.exceededQuota = true; |
| console.warn('Local storage quota exceeded: disabling'); |
| return; |
| } |
| } |
| throw exc; |
| } |
| } |
| } |