boundedcollectablestorage.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. // Copyright 2013 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Provides a convenient API for data persistence with data
  16. * expiration and number of items limit.
  17. *
  18. * Setting and removing values keeps a max number of items invariant.
  19. * Collecting values can be user initiated. If oversize, first removes
  20. * expired items, if still oversize than removes the oldest items until a size
  21. * constraint is fulfilled.
  22. *
  23. */
  24. goog.provide('goog.labs.storage.BoundedCollectableStorage');
  25. goog.require('goog.array');
  26. goog.require('goog.asserts');
  27. goog.require('goog.iter');
  28. goog.require('goog.storage.CollectableStorage');
  29. goog.require('goog.storage.ErrorCode');
  30. goog.require('goog.storage.ExpiringStorage');
  31. /**
  32. * Provides a storage with bounded number of elements, expiring keys and
  33. * a collection method.
  34. *
  35. * @param {!goog.storage.mechanism.IterableMechanism} mechanism The underlying
  36. * storage mechanism.
  37. * @param {number} maxItems Maximum number of items in storage.
  38. * @constructor
  39. * @struct
  40. * @extends {goog.storage.CollectableStorage}
  41. * @final
  42. */
  43. goog.labs.storage.BoundedCollectableStorage = function(mechanism, maxItems) {
  44. goog.labs.storage.BoundedCollectableStorage.base(
  45. this, 'constructor', mechanism);
  46. /**
  47. * A maximum number of items that should be stored.
  48. * @private {number}
  49. */
  50. this.maxItems_ = maxItems;
  51. };
  52. goog.inherits(
  53. goog.labs.storage.BoundedCollectableStorage,
  54. goog.storage.CollectableStorage);
  55. /**
  56. * An item key used to store a list of keys.
  57. * @const
  58. * @private
  59. */
  60. goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_ =
  61. 'bounded-collectable-storage';
  62. /**
  63. * Recreates a list of keys in order of creation.
  64. *
  65. * @return {!Array<string>} a list of unexpired keys.
  66. * @private
  67. */
  68. goog.labs.storage.BoundedCollectableStorage.prototype.rebuildIndex_ =
  69. function() {
  70. var keys = [];
  71. goog.iter.forEach(
  72. /** @type {goog.storage.mechanism.IterableMechanism} */ (this.mechanism)
  73. .__iterator__(true),
  74. function(key) {
  75. if (goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_ == key) {
  76. return;
  77. }
  78. var wrapper;
  79. try {
  80. wrapper = this.getWrapper(key, true);
  81. } catch (ex) {
  82. if (ex == goog.storage.ErrorCode.INVALID_VALUE) {
  83. // Skip over bad wrappers and continue.
  84. return;
  85. }
  86. // Unknown error, escalate.
  87. throw ex;
  88. }
  89. goog.asserts.assert(wrapper);
  90. var creationTime =
  91. goog.storage.ExpiringStorage.getCreationTime(wrapper);
  92. keys.push({key: key, created: creationTime});
  93. },
  94. this);
  95. goog.array.sort(keys, function(a, b) { return a.created - b.created; });
  96. return goog.array.map(keys, function(v) { return v.key; });
  97. };
  98. /**
  99. * Gets key list from a local storage. If an item does not exist,
  100. * may recreate it.
  101. *
  102. * @param {boolean} rebuild Whether to rebuild a index if no index item exists.
  103. * @return {!Array<string>} a list of keys if index exist, otherwise undefined.
  104. * @private
  105. */
  106. goog.labs.storage.BoundedCollectableStorage.prototype.getKeys_ = function(
  107. rebuild) {
  108. var keys =
  109. goog.labs.storage.BoundedCollectableStorage.superClass_.get.call(
  110. this, goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_) ||
  111. null;
  112. if (!keys || !goog.isArray(keys)) {
  113. if (rebuild) {
  114. keys = this.rebuildIndex_();
  115. } else {
  116. keys = [];
  117. }
  118. }
  119. return /** @type {!Array<string>} */ (keys);
  120. };
  121. /**
  122. * Saves a list of keys in a local storage.
  123. *
  124. * @param {Array<string>} keys a list of keys to save.
  125. * @private
  126. */
  127. goog.labs.storage.BoundedCollectableStorage.prototype.setKeys_ = function(
  128. keys) {
  129. goog.labs.storage.BoundedCollectableStorage.superClass_.set.call(
  130. this, goog.labs.storage.BoundedCollectableStorage.KEY_LIST_KEY_, keys);
  131. };
  132. /**
  133. * Remove subsequence from a sequence.
  134. *
  135. * @param {!Array<string>} keys is a sequence.
  136. * @param {!Array<string>} keysToRemove subsequence of keys, the order must
  137. * be kept.
  138. * @return {!Array<string>} a keys sequence after removing keysToRemove.
  139. * @private
  140. */
  141. goog.labs.storage.BoundedCollectableStorage.removeSubsequence_ = function(
  142. keys, keysToRemove) {
  143. if (keysToRemove.length == 0) {
  144. return goog.array.clone(keys);
  145. }
  146. var keysToKeep = [];
  147. var keysIdx = 0;
  148. var keysToRemoveIdx = 0;
  149. while (keysToRemoveIdx < keysToRemove.length && keysIdx < keys.length) {
  150. var key = keysToRemove[keysToRemoveIdx];
  151. while (keysIdx < keys.length && keys[keysIdx] != key) {
  152. keysToKeep.push(keys[keysIdx]);
  153. ++keysIdx;
  154. }
  155. ++keysToRemoveIdx;
  156. }
  157. goog.asserts.assert(keysToRemoveIdx == keysToRemove.length);
  158. goog.asserts.assert(keysIdx < keys.length);
  159. return goog.array.concat(keysToKeep, goog.array.slice(keys, keysIdx + 1));
  160. };
  161. /**
  162. * Keeps the number of items in storage under maxItems. Removes elements in the
  163. * order of creation.
  164. *
  165. * @param {!Array<string>} keys a list of keys in order of creation.
  166. * @param {number} maxSize a number of items to keep.
  167. * @return {!Array<string>} keys left after removing oversize data.
  168. * @private
  169. */
  170. goog.labs.storage.BoundedCollectableStorage.prototype.collectOversize_ =
  171. function(keys, maxSize) {
  172. if (keys.length <= maxSize) {
  173. return goog.array.clone(keys);
  174. }
  175. var keysToRemove = goog.array.slice(keys, 0, keys.length - maxSize);
  176. goog.array.forEach(keysToRemove, function(key) {
  177. goog.labs.storage.BoundedCollectableStorage.superClass_.remove.call(
  178. this, key);
  179. }, this);
  180. return goog.labs.storage.BoundedCollectableStorage.removeSubsequence_(
  181. keys, keysToRemove);
  182. };
  183. /**
  184. * Cleans up the storage by removing expired keys.
  185. *
  186. * @param {boolean=} opt_strict Also remove invalid keys.
  187. * @override
  188. */
  189. goog.labs.storage.BoundedCollectableStorage.prototype.collect = function(
  190. opt_strict) {
  191. var keys = this.getKeys_(true);
  192. var keysToRemove = this.collectInternal(keys, opt_strict);
  193. keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_(
  194. keys, keysToRemove);
  195. this.setKeys_(keys);
  196. };
  197. /**
  198. * Ensures that we keep only maxItems number of items in a local storage.
  199. * @param {boolean=} opt_skipExpired skip removing expired items first.
  200. * @param {boolean=} opt_strict Also remove invalid keys.
  201. */
  202. goog.labs.storage.BoundedCollectableStorage.prototype.collectOversize =
  203. function(opt_skipExpired, opt_strict) {
  204. var keys = this.getKeys_(true);
  205. if (!opt_skipExpired) {
  206. var keysToRemove = this.collectInternal(keys, opt_strict);
  207. keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_(
  208. keys, keysToRemove);
  209. }
  210. keys = this.collectOversize_(keys, this.maxItems_);
  211. this.setKeys_(keys);
  212. };
  213. /**
  214. * Set an item in the storage.
  215. *
  216. * @param {string} key The key to set.
  217. * @param {*} value The value to serialize to a string and save.
  218. * @param {number=} opt_expiration The number of miliseconds since epoch
  219. * (as in goog.now()) when the value is to expire. If the expiration
  220. * time is not provided, the value will persist as long as possible.
  221. * @override
  222. */
  223. goog.labs.storage.BoundedCollectableStorage.prototype.set = function(
  224. key, value, opt_expiration) {
  225. goog.labs.storage.BoundedCollectableStorage.base(
  226. this, 'set', key, value, opt_expiration);
  227. var keys = this.getKeys_(true);
  228. goog.array.remove(keys, key);
  229. if (goog.isDef(value)) {
  230. keys.push(key);
  231. if (keys.length >= this.maxItems_) {
  232. var keysToRemove = this.collectInternal(keys);
  233. keys = goog.labs.storage.BoundedCollectableStorage.removeSubsequence_(
  234. keys, keysToRemove);
  235. keys = this.collectOversize_(keys, this.maxItems_);
  236. }
  237. }
  238. this.setKeys_(keys);
  239. };
  240. /**
  241. * Remove an item from the data storage.
  242. *
  243. * @param {string} key The key to remove.
  244. * @override
  245. */
  246. goog.labs.storage.BoundedCollectableStorage.prototype.remove = function(key) {
  247. goog.labs.storage.BoundedCollectableStorage.base(this, 'remove', key);
  248. var keys = this.getKeys_(false);
  249. if (goog.isDef(keys)) {
  250. goog.array.remove(keys, key);
  251. this.setKeys_(keys);
  252. }
  253. };