/
persistent_storage.js
147 lines (112 loc) · 3.12 KB
/
persistent_storage.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var PersistentStorage = (function() {
'use strict';
var LOCAL_STORAGE;
try {
LOCAL_STORAGE = window.localStorage;
// while in private browsing mode, some browsers make
// localStorage available, but throw an error when used
LOCAL_STORAGE.setItem('~~~', '!');
LOCAL_STORAGE.removeItem('~~~');
} catch (err) {
LOCAL_STORAGE = null;
}
// constructor
// -----------
function PersistentStorage(namespace, override) {
this.prefix = ['__', namespace, '__'].join('');
this.ttlKey = '__ttl__';
this.keyMatcher = new RegExp('^' + _.escapeRegExChars(this.prefix));
// for testing purpose
this.ls = override || LOCAL_STORAGE;
// if local storage isn't available, everything becomes a noop
!this.ls && this._noop();
}
// instance methods
// ----------------
_.mixin(PersistentStorage.prototype, {
// ### private
_prefix: function(key) {
return this.prefix + key;
},
_ttlKey: function(key) {
return this._prefix(key) + this.ttlKey;
},
_noop: function() {
this.get =
this.set =
this.remove =
this.clear =
this.isExpired = _.noop;
},
_safeSet: function(key, val) {
try {
this.ls.setItem(key, val);
} catch (err) {
// hit the localstorage limit so clean up and better luck next time
if (err.name === 'QuotaExceededError') {
this.clear();
this._noop();
}
}
},
// ### public
get: function(key) {
if (this.isExpired(key)) {
this.remove(key);
}
return decode(this.ls.getItem(this._prefix(key)));
},
set: function(key, val, ttl) {
if (_.isNumber(ttl)) {
this._safeSet(this._ttlKey(key), encode(now() + ttl));
}
else {
this.ls.removeItem(this._ttlKey(key));
}
return this._safeSet(this._prefix(key), encode(val));
},
remove: function(key) {
this.ls.removeItem(this._ttlKey(key));
this.ls.removeItem(this._prefix(key));
return this;
},
clear: function() {
var i, keys = gatherMatchingKeys(this.keyMatcher);
for (i = keys.length; i--;) {
this.remove(keys[i]);
}
return this;
},
isExpired: function(key) {
var ttl = decode(this.ls.getItem(this._ttlKey(key)));
return _.isNumber(ttl) && now() > ttl ? true : false;
}
});
return PersistentStorage;
// helper functions
// ----------------
function now() {
return new Date().getTime();
}
function encode(val) {
// convert undefined to null to avoid issues with JSON.parse
return JSON.stringify(_.isUndefined(val) ? null : val);
}
function decode(val) {
return $.parseJSON(val);
}
function gatherMatchingKeys(keyMatcher) {
var i, key, keys = [], len = LOCAL_STORAGE.length;
for (i = 0; i < len; i++) {
if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
keys.push(key.replace(keyMatcher, ''));
}
}
return keys;
}
})();