-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfetchCache.js
161 lines (144 loc) · 6.07 KB
/
fetchCache.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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Copyright (c) Christopher Keefer, 2016.
* https://github.com/SaneMethod/fetchCache
*
* Override fetch in the global context to allow us to cache the response to fetch in a Storage interface
* implementing object (such as localStorage).
*/
(function (fetch) {
/* If the context doesn't support fetch, we won't attempt to patch in our
caching using fetch, for obvious reasons. */
if (!fetch) return;
/**
* Generate the cache key under which to store the local data - either the cache key supplied,
* or one generated from the url, the Content-type header (if specified) and the body (if specified).
*
* @returns {string}
*/
function genCacheKey(url, settings) {
var {headers:{'Content-type': type}} = ('headers' in settings) ? settings : {headers: {}},
{body} = settings;
return settings.cacheKey || url + (type || '') + (body || '');
}
/**
* Determine whether we're using localStorage or, if the user has specified something other than a boolean
* value for options.localCache, whether the value appears to satisfy the plugin's requirements.
* Otherwise, throw a new TypeError indicating what type of value we expect.
*
* @param {boolean|object} storage
* @returns {boolean|object}
*/
function getStorage(storage) {
if (!storage) return false;
if (storage === true) return self.localStorage;
if (typeof storage === "object" && 'getItem' in storage &&
'removeItem' in storage && 'setItem' in storage) {
return storage;
}
throw new TypeError("localCache must either be a boolean value, " +
"or an object which implements the Storage interface.");
}
/**
* Remove the item specified by cacheKey and its attendant meta items from storage.
*
* @param {Storage|object} storage
* @param {string} cacheKey
*/
function removeFromStorage(storage, cacheKey) {
storage.removeItem(cacheKey);
storage.removeItem(cacheKey + 'cachettl');
storage.removeItem(cacheKey + 'dataType');
}
/**
* Cache the response into our storage object.
* We clone the response so that we can drain the stream without making it
* unavailable to future handlers.
*
* @param {string} cacheKey Key under which to cache the data string. Bound in
* fetch override.
* @param {Storage} storage Object implementing Storage interface to store cached data
* (text or json exclusively) in. Bound in fetch override.
* @param {Number} hourstl Number of hours this value shoud remain in the cache.
* Bound in fetch override.
* @param {Response} response
*/
function cacheResponse(cacheKey, storage, hourstl, response) {
var cres = response.clone(),
dataType = (response.headers.get('Content-Type') || 'text/plain').toLowerCase();
cres.text().then((text) => {
try {
storage.setItem(cacheKey, text);
storage.setItem(cacheKey + 'cachettl', +new Date() + 1000 * 60 * 60 * hourstl);
storage.setItem(cacheKey + 'dataType', dataType);
} catch (e) {
// Remove any incomplete data that may have been saved before the exception was caught
removeFromStorage(storage, cacheKey);
console.log('Cache Error: ' + e, cacheKey, text);
}
});
return response;
}
/**
* Create a new response containing the cached value, and return a promise
* that resolves with this response.
*
* @param value
* @param dataType
* @returns {Promise}
*/
function provideResponse(value, dataType) {
var response = new Response(
value,
{
status: 200,
statusText: 'success',
headers: {
'Content-Type': dataType
}
}
);
return new Promise(function (resolve, reject) {
resolve(response);
});
}
/**
* Override fetch on the global context, so that we can intercept
* fetch calls and respond with locally cached content, if available.
* New parameters available on the call to fetch:
* localCache : true // required - either a boolean (if true, localStorage is used,
* if false request is not cached or returned from cache), or an object implementing the
* Storage interface, in which case that object is used instead.
* cacheTTL : 5, // optional, cache time in hours, default is 5. Use float numbers for
* values less than a full hour (e.g. `0.5` for 1/2 hour).
* cacheKey : 'post', // optional - key under which cached string will be stored.
* isCacheValid : function // optional - return true for valid, false for invalid.
*/
self.fetch = function (url, settings) {
var storage = getStorage(settings.localCache),
hourstl = settings.cacheTTL || 5,
cacheKey = genCacheKey(url, settings),
cacheValid = settings.isCacheValid,
ttl,
value,
dataType;
if (!storage) return fetch(url, settings);
ttl = storage.getItem(cacheKey + 'cachettl');
if (cacheValid && typeof cacheValid === 'function' && !cacheValid()) {
removeFromStorage(storage, cacheKey);
ttl = 0;
}
if (ttl && ttl < +new Date()) {
removeFromStorage(storage, cacheKey);
}
value = storage.getItem(cacheKey);
if (!value) {
/* If not cached, we'll make the request and add a then block to the resulting promise,
in which we'll cache the result. */
return fetch(url, settings).then(cacheResponse.bind(null, cacheKey, storage, hourstl));
}
/* Value is cached, so we'll simply create and respond with a promise of our own,
and provide a response object. */
dataType = storage.getItem(cacheKey + 'dataType') || 'text/plain';
return provideResponse(value, dataType);
};
})(self.fetch);