// Copyright 2013 The Closure Library Authors. All Rights Reserved. // // 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. /** * @fileoverview Provides a convenient API for data persistence with data * expiration and number of items limit. * * Setting and removing values keeps a max number of items invariant. * Collecting values can be user initiated. If oversize, first removes * expired items, if still oversize than removes the oldest items until a size * constraint is fulfilled. * */ goog.provide('goog.labs.storage.BoundedCollectableStorage'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.iter'); goog.require('goog.storage.CollectableStorage'); goog.require('goog.storage.ErrorCode'); goog.require('goog.storage.ExpiringStorage'); /** * Provides a storage with bounded number of elements, expiring keys and * a collection method. * * @param {!goog.storage.mechanism.IterableMechanism} mechanism The underlying * storage mechanism. * @param {number} maxItems Maximum number of items in storage. * @constructor * @struct * @extends {goog.storage.CollectableStorage} * @final */ goog.labs.storage.BoundedCollectableStorage = function(mechanism, maxItems) { goog.labs.storage.BoundedCollectableStorage.base( this, 'constructor', mechanism); /** * A maximum number of items that should be stored. * @private {number} */ this.maxItems_ = maxItems; }; goog.inherits( goog.labs.storage.BoundedCollectableStorage, goog.storage.CollectableStorage); /** * An item key used to store a list of keys. * @const * @private */ goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_ = 'bounded-collectable-storage'; /** * Recreates a list of keys in order of creation. * * @return {!Array} a list of unexpired keys. * @private */ goog.labs.storage.BoundedCollectableStorage.prototype.rebuildIndex_ = function() { var keys = []; goog.iter.forEach( /** @type {goog.storage.mechanism.IterableMechanism} */ (this.mechanism) .__iterator__(true), function(key) { if (goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_ == key) { return; } var wrapper; try { wrapper = this.getWrapper(key, true); } catch (ex) { if (ex == goog.storage.ErrorCode.INVALID_VALUE) { // Skip over bad wrappers and continue. return; } // Unknown error, escalate. throw ex; } goog.asserts.assert(wrapper); var creationTime = goog.storage.ExpiringStorage.getCreationTime(wrapper); keys.push({key: key, created: creationTime}); }, this); goog.array.sort(keys, function(a, b) { return a.created - b.created; }); return goog.array.map(keys, function(v) { return v.key; }); }; /** * Gets key list from a local storage. If an item does not exist, * may recreate it. * * @param {boolean} rebuild Whether to rebuild a index if no index item exists. * @return {!Array} a list of keys if index exist, otherwise undefined. * @private */ goog.labs.storage.BoundedCollectableStorage.prototype.getKeys_ = function( rebuild) { var keys = goog.labs.storage.BoundedCollectableStorage.superClass_.get.call( this, goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_) || null; if (!keys || !goog.isArray(keys)) { if (rebuild) { keys = this.rebuildIndex_(); } else { keys = []; } } return /** @type {!Array} */ (keys); }; /** * Saves a list of keys in a local storage. * * @param {Array} keys a list of keys to save. * @private */ goog.labs.storage.BoundedCollectableStorage.prototype.setKeys_ = function( keys) { goog.labs.storage.BoundedCollectableStorage.superClass_.set.call( this, goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_, keys); }; /** * Remove subsequence from a sequence. * * @param {!Array} keys is a sequence. * @param {!Array} keysToRemove subsequence of keys, the order must * be kept. * @return {!Array} a keys sequence after removing keysToRemove. * @private */ goog.labs.storage.BoundedCollectableStorage.removeSubsequence_ = function( keys, keysToRemove) { if (keysToRemove.length == 0) { return goog.array.clone(keys); } var keysToKeep = []; var keysIdx = 0; var keysToRemoveIdx = 0; while (keysToRemoveIdx < keysToRemove.length && keysIdx < keys.length) { var key = keysToRemove[keysToRemoveIdx]; while (keysIdx < keys.length && keys[keysIdx] != key) { keysToKeep.push(keys[keysIdx]); ++keysIdx; } ++keysToRemoveIdx; } goog.asserts.assert(keysToRemoveIdx == keysToRemove.length); goog.asserts.assert(keysIdx < keys.length); return goog.array.concat(keysToKeep, goog.array.slice(keys, keysIdx + 1)); }; /** * Keeps the number of items in storage under maxItems. Removes elements in the * order of creation. * * @param {!Array} keys a list of keys in order of creation. * @param {number} maxSize a number of items to keep. * @return {!Array} keys left after removing oversize data. * @private */ goog.labs.storage.BoundedCollectableStorage.prototype.collectOversize_ = function(keys, maxSize) { if (keys.length <= maxSize) { return goog.array.clone(keys); } var keysToRemove = goog.array.slice(keys, 0, keys.length - maxSize); goog.array.forEach(keysToRemove, function(key) { goog.labs.storage.BoundedCollectableStorage.superClass_.remove.call( this, key); }, this); return goog.labs.storage.BoundedCollectableStorage.removeSubsequence_( keys, keysToRemove); }; /** * Cleans up the storage by removing expired keys. * * @param {boolean=} opt_strict Also remove invalid keys. * @override */ goog.labs.storage.BoundedCollectableStorage.prototype.collect = function( opt_strict) { var keys = this.getKeys_(true); var keysToRemove = this.collectInternal(keys, opt_strict); keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_( keys, keysToRemove); this.setKeys_(keys); }; /** * Ensures that we keep only maxItems number of items in a local storage. * @param {boolean=} opt_skipExpired skip removing expired items first. * @param {boolean=} opt_strict Also remove invalid keys. */ goog.labs.storage.BoundedCollectableStorage.prototype.collectOversize = function(opt_skipExpired, opt_strict) { var keys = this.getKeys_(true); if (!opt_skipExpired) { var keysToRemove = this.collectInternal(keys, opt_strict); keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_( keys, keysToRemove); } keys = this.collectOversize_(keys, this.maxItems_); this.setKeys_(keys); }; /** * Set an item in the storage. * * @param {string} key The key to set. * @param {*} value The value to serialize to a string and save. * @param {number=} opt_expiration The number of miliseconds since epoch * (as in goog.now()) when the value is to expire. If the expiration * time is not provided, the value will persist as long as possible. * @override */ goog.labs.storage.BoundedCollectableStorage.prototype.set = function( key, value, opt_expiration) { goog.labs.storage.BoundedCollectableStorage.base( this, 'set', key, value, opt_expiration); var keys = this.getKeys_(true); goog.array.remove(keys, key); if (goog.isDef(value)) { keys.push(key); if (keys.length >= this.maxItems_) { var keysToRemove = this.collectInternal(keys); keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_( keys, keysToRemove); keys = this.collectOversize_(keys, this.maxItems_); } } this.setKeys_(keys); }; /** * Remove an item from the data storage. * * @param {string} key The key to remove. * @override */ goog.labs.storage.BoundedCollectableStorage.prototype.remove = function(key) { goog.labs.storage.BoundedCollectableStorage.base(this, 'remove', key); var keys = this.getKeys_(false); if (goog.isDef(keys)) { goog.array.remove(keys, key); this.setKeys_(keys); } };