· 8 years ago · Apr 18, 2018, 07:34 PM
1/**
2 * Implements a persistent local key/value data store similar to HTML5's
3 * localStorage. Should work in IE5+, Firefox 2+, Safari 3.1+, and any browser
4 * with Google Gears installed (including Chrome). Doesn't work in Opera.
5 *
6 * @module storage
7 * @namespace YAHOO.Search
8 * @requires yahoo, event, json
9 */
10(function () {
11 // Shorthand.
12 var d = document,
13 w = window,
14 Y = YAHOO,
15 YS = Y.namespace('Search'),
16 yut = Y.util,
17 JSON = window.JSON || Y.lang.JSON,
18 Event = yut.Event,
19
20 // -- Private Constants ----------------------------------------------------
21 DB_NAME = 'ysearch_storage',
22 DB_DISPLAYNAME = 'Yahoo! Search Storage',
23 DB_MAXSIZE = 1048576,
24 DB_VERSION = '1.0',
25
26 USERDATA_PATH = 'ysearch',
27 USERDATA_NAME = 'data',
28
29 // -- Private Variables ----------------------------------------------------
30 data = {}, ready = false, self, storage;
31
32 // -- Storage Classes ------------------------------------------------------
33
34 YS.StorageFullError = function (message) {
35 YS.StorageFullError.superclass.constructor.call(message);
36
37 this.name = 'StorageFullError';
38 this.message = message || 'Maximum storage capacity reached';
39
40 if (Y.env.ua.ie) {
41 this.description = this.message;
42 }
43 };
44
45 Y.lang.extend(YS.StorageFullError, Error);
46
47 /**
48 * The StorageInterface class defines the interface that all storage
49 * implementations will adhere to. This is also the noop fallback for
50 * browsers that don't support an actual storage implementation.
51 *
52 * @class StorageInterface
53 * @constructor
54 * @private
55 */
56 function StorageInterface() {
57 this.onStorageReady.subscribeEvent.subscribe(function (e, args) {
58 var fn = args[0],
59 obj = args[1],
60 overrideScope = args[2];
61
62 if (ready && fn) {
63 // Sadly, subscribeEvent is broken in that it doesn't give us
64 // any reliable way of calling the new subscriber exactly the
65 // same way that CustomEvent.fire would. This sucks. So we'll
66 // just have to do our best.
67
68 if (obj && overrideScope) {
69 fn.call(obj);
70 } else {
71 fn.call(window, obj);
72 }
73 }
74 });
75 }
76
77 // TODO: storage events
78 // TODO: event when approaching storage limit
79
80 StorageInterface.prototype = {
81 // -- Public Events ----------------------------------------------------
82
83 /**
84 * Fired when the storage interface is loaded and ready for use.
85 *
86 * @event onStorageReady
87 * @type CustomEvent
88 */
89 onStorageReady: new yut.CustomEvent('storageReady'),
90
91 // -- Public Methods ---------------------------------------------------
92
93 /**
94 * Removes all items from the data store.
95 *
96 * @method clear
97 */
98 clear: function () {},
99
100 /**
101 * Returns the item with the specified key, or <code>null</code> if the
102 * item was not found.
103 *
104 * @method getItem
105 * @param {String} key
106 * @param {bool} json (optional) <code>true</code> if the item is a JSON
107 * string and should be parsed before being returned
108 * @return {Object|null} item or <code>null</code> if not found
109 */
110 getItem: function (key, json) { return null; },
111
112 /**
113 * Returns the number of items in the data store.
114 *
115 * @method length
116 * @return {Number} number of items in the data store
117 */
118 length: function () { return 0; },
119
120 /**
121 * Removes the item with the specified key.
122 *
123 * @method removeItem
124 * @param {String} key
125 */
126 removeItem: function (key) {},
127
128 /**
129 * Stores an item under the specified key. If the key already exists in
130 * the data store, it will be replaced.
131 *
132 * @method setItem
133 * @param {String} key
134 * @param {Object} value
135 * @param {bool} json (optional) <code>true</code> if the item should be
136 * serialized to a JSON string before being stored
137 */
138 setItem: function (key, value, json) {}
139 };
140
141 /**
142 * The DatabaseStorage class provides a SQLite-based local data store for
143 * Safari 3.1 and 3.2.
144 *
145 * @class DatabaseStorage
146 * @uses StorageInterface
147 * @constructor
148 * @private
149 */
150 function DatabaseStorage() {
151 self = this;
152
153 StorageInterface.call(self);
154
155 self._open();
156 self._create();
157 }
158
159 DatabaseStorage.prototype = {
160 clear: function () {
161 data = {};
162 self._save();
163 },
164
165 getItem: function (key, json) {
166 return data.hasOwnProperty(key) ? data[key] : null;
167 },
168
169 length: function () {
170 var count = 0, key;
171
172 for (key in data) {
173 if (data.hasOwnProperty(key)) {
174 count += 1;
175 }
176 }
177
178 return count;
179 },
180
181 removeItem: function (key) {
182 delete(data[key]);
183 self._save();
184 },
185
186 setItem: function (key, value, json) {
187 data[key] = value;
188 self._save();
189 },
190
191 _create: function () {
192 storage.transaction(function (t) {
193 t.executeSql("CREATE TABLE IF NOT EXISTS ysearch_storage(name TEXT PRIMARY KEY, value TEXT NOT NULL)");
194 t.executeSql("SELECT value FROM ysearch_storage WHERE name = 'data'", [], self._load);
195 });
196 },
197
198 _load: function (t, results) {
199 if (results.rows.length) {
200 try {
201 data = JSON.parse(results.rows.item(0).value);
202 } catch (e) {
203 data = {};
204 }
205 }
206
207 ready = true;
208 self.onStorageReady.fire();
209 },
210
211 _open: function () {
212 storage = w.openDatabase(DB_NAME, DB_VERSION, DB_DISPLAYNAME, DB_MAXSIZE);
213 },
214
215 _save: function () {
216 storage.transaction(function (t) {
217 t.executeSql("REPLACE INTO ysearch_storage (name, value) VALUES ('data', ?)", [JSON.stringify(data)]);
218 });
219 }
220 };
221
222 /**
223 * The GearsStorage class provides a Google Gears-based local data store for
224 * Google Chrome and any browser with Google Gears installed.
225 *
226 * @class GearsStorage
227 * @uses DatabaseStorage
228 * @constructor
229 * @private
230 */
231 function GearsStorage() {
232 self = this;
233
234 StorageInterface.call(self);
235
236 self._open();
237 self._create();
238 }
239
240 GearsStorage.prototype = {
241 _create: function () {
242 storage.execute("CREATE TABLE IF NOT EXISTS ysearch_storage(name TEXT PRIMARY KEY, value TEXT NOT NULL)");
243 self._load(storage.execute("SELECT value FROM ysearch_storage WHERE name = 'data'"));
244 },
245
246 _load: function (results) {
247 if (results.isValidRow() && results.fieldCount()) {
248 try {
249 data = JSON.parse(results.field(0));
250 } catch (e) {
251 data = {};
252 }
253 }
254
255 ready = true;
256 self.onStorageReady.fire();
257 },
258
259 _open: function () {
260 storage = google.gears.factory.create('beta.database');
261 storage.open(DB_NAME);
262 },
263
264 _save: function () {
265 var retries = 0,
266 store = function () {
267 try {
268 storage.execute("REPLACE INTO ysearch_storage (name, value) VALUES ('data', ?)", [JSON.stringify(data)]);
269 } catch (e) {
270 // Gears database write operations can fail if multiple
271 // processes attempt to write to the database at the
272 // same time. Since Gears apparently can't be bothered
273 // to handle this case on its own like any reasonable
274 // database would, we have to deal with it.
275
276 if (retries > 2) {
277 throw e;
278 }
279
280 setTimeout(store, 50 * (retries += 1));
281 }
282 };
283
284 store();
285 }
286 };
287
288 /**
289 * The GeckoStorage class provides a globalStorage-based local data store
290 * for Firefox 2 and 3.0.
291 *
292 * @class GeckoStorage
293 * @uses HTML5Storage
294 * @constructor
295 * @private
296 */
297 function GeckoStorage() {
298 StorageInterface.call(this);
299
300 storage = w.globalStorage[w.location.hostname];
301 ready = true;
302
303 this.onStorageReady.fire();
304 }
305
306 GeckoStorage.prototype = {
307 clear: function () {
308 for (var key in storage) {
309 if (storage.hasOwnProperty(key)) {
310 storage.removeItem(key);
311 }
312 }
313 },
314
315 getItem: function (key, json) {
316 try {
317 return json ? JSON.parse(storage[key].value) :
318 storage[key].value;
319 } catch (e) {
320 return null;
321 }
322 }
323 };
324
325 /**
326 * The HTML5Storage class provides a localStorage-based local data store for
327 * browsers that support HTML5 storage (currently IE8, Firefox 3.5, and
328 * Safari 4).
329 *
330 * @class HTML5Storage
331 * @uses StorageInterface
332 * @constructor
333 * @private
334 */
335 function HTML5Storage() {
336 StorageInterface.call(this);
337
338 storage = w.localStorage;
339 ready = true;
340
341 this.onStorageReady.fire();
342 }
343
344 HTML5Storage.prototype = {
345 clear: function () {
346 storage.clear();
347 },
348
349 getItem: function (key, json) {
350 try {
351 return json ? JSON.parse(storage.getItem(key)) :
352 storage.getItem(key);
353 } catch (e) {
354 return null;
355 }
356 },
357
358 length: function () {
359 return storage.length;
360 },
361
362 removeItem: function (key) {
363 storage.removeItem(key);
364 },
365
366 setItem: function (key, value, json) {
367 storage.setItem(key, json ? JSON.stringify(value) : value);
368 }
369 };
370
371 /**
372 * The UserDataStorage class provides a userData-based local data store for
373 * IE5, 6, and 7.
374 *
375 * @class UserDataStorage
376 * @uses DatabaseStorage
377 * @constructor
378 * @private
379 */
380 function UserDataStorage() {
381 self = this;
382
383 StorageInterface.call(self);
384
385 storage = d.createElement('span');
386 storage.addBehavior('#default#userData');
387
388 Event.onDOMReady(function () {
389 d.body.appendChild(storage);
390 storage.load(USERDATA_PATH);
391
392 try {
393 data = JSON.parse(storage.getAttribute(USERDATA_NAME));
394 } catch (e) {
395 data = {};
396 }
397
398 ready = true;
399 self.onStorageReady.fire();
400 });
401 }
402
403 UserDataStorage.prototype = {
404 _save: function () {
405 var _data = JSON.stringify(data);
406
407 try {
408 storage.setAttribute(USERDATA_NAME, _data);
409 storage.save(USERDATA_PATH);
410 } catch (e) {
411 throw new YS.StorageFullError();
412 }
413 }
414 };
415
416 /**
417 * Provides a persistent local key/value data store similar to HTML5's
418 * localStorage.
419 *
420 * @class Storage
421 * @uses StorageInterface
422 * @static
423 */
424
425 Y.lang.augmentProto(DatabaseStorage, StorageInterface);
426 Y.lang.augmentProto(HTML5Storage, StorageInterface);
427 Y.lang.augmentProto(GearsStorage, DatabaseStorage);
428 Y.lang.augmentProto(GeckoStorage, HTML5Storage);
429 Y.lang.augmentProto(UserDataStorage, DatabaseStorage);
430
431 if (w.localStorage) {
432 YS.Storage = new HTML5Storage();
433 } else if (w.globalStorage) {
434 YS.Storage = new GeckoStorage();
435 } else if (w.openDatabase && navigator.userAgent.indexOf('Chrome') === -1) {
436 YS.Storage = new DatabaseStorage();
437 } else if (w.google && w.google.gears) {
438 YS.Storage = new GearsStorage();
439 } else if (Y.env.ua.ie >= 5) {
440 YS.Storage = new UserDataStorage();
441 } else {
442 // This browser doesn't support any of the actual storage
443 // implementations, so we'll give it the noop interface.
444 YS.Storage = new StorageInterface();
445 }
446})();