Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: interpretation error, misspelled parameter name #17

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dc7635e
Bugfix: syntax error, misspelled variable name
rezgar Dec 11, 2023
c7cf4b4
Cached value added to debug logging
rezgar Dec 11, 2023
61eea13
Stringify cache value in logging
rezgar Dec 11, 2023
ee2c257
Minor refactoring for clarity. Attempt to get POSTs working with body
rezgar Dec 12, 2023
34219b3
Bug fix: pass send arguments to downstream send vs on hit/miss handle…
rezgar Dec 12, 2023
2c77cb7
Logging verbosity reverted
rezgar Dec 12, 2023
301e064
Proceed with request on corrupted cache data
rezgar Dec 12, 2023
29598f5
Making sure responseText property corresponds to response
rezgar Dec 12, 2023
029b0b5
Revert "Making sure responseText property corresponds to response"
rezgar Dec 12, 2023
3adfc2e
Attempt to add request/response heaers
rezgar Dec 12, 2023
dfcf5d5
Response parsed based on content-type header
rezgar Dec 12, 2023
0128918
Allow for manually set response headers on cache hit
rezgar Dec 12, 2023
7dcb8bd
Request headers made consistent with response headers
rezgar Dec 13, 2023
6431121
Cache key mangler considers method and body
rezgar Dec 15, 2023
756cb3a
Moved cache key composition to proxy, allow customization based on me…
rezgar Dec 15, 2023
4629161
Cache selector and key composers receive more request details as input
rezgar Dec 15, 2023
16af2d3
Normalize host/path case
rezgar Dec 15, 2023
26a7e51
URL construction - add origin
rezgar Dec 15, 2023
e601038
Revert: don't attempt to parse request body, format unknown
rezgar Dec 15, 2023
c5f50ab
Property rename
rezgar Dec 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions lib/request-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ function RequestCache(options) {
}

function handleCacheMiss(key, onMiss) {
if (this.options.debugCacheMiss) console.log('[Teuthis] proxy-miss ' + key);
if (this.options.debugCacheMiss) console.log('[Teuthis] proxy-miss (#' + (this.stats.miss + 1) + '): ' + key);
this.stats.miss ++;
if (_.isFunction(onMiss)) onMiss(key);
if (_.isFunction(this.options.onStatus)) this.options.onStatus.call(this);
}

function handleCacheHit(key, cachedValue, onHit) {
if (this.options.debugCacheHits) console.log('[Teuthis] proxy-hit ' + key);
if (this.options.debugCacheHits) console.log('[Teuthis] proxy-hit (#' + (this.stats.hits + 1) + '): ' + key);
this.stats.hits ++;
if (_.isFunction(onHit)) onHit(key, cachedValue);
if (_.isFunction(this.options.onStatus)) this.options.onStatus.call(this);
Expand All @@ -93,8 +93,8 @@ RequestCache.prototype.weakLength = function() {
return Object.getOwnPropertyNames(this.cacheKeys).length;
}

RequestCache.prototype.composeKey = function(method, url) {
return this.keyPrefix + method + '__' + url;
RequestCache.prototype.composeKey = function(cacheKey) {
return this.keyPrefix + cacheKey;
}

RequestCache.prototype.keyIsPrefixed = function(key) {
Expand Down Expand Up @@ -164,29 +164,29 @@ RequestCache.prototype.each = function(cb, done) {

// Weakly check if object is cached. Only checks our key list, not localforage itself
// So does not guarantee match(...) will have a HIT if, say, someone cleared localforage
RequestCache.prototype.weakHas = function(method, url) {
var key = this.composeKey(method, url);
return this.cacheKeys.hasOwnProperty(key);
RequestCache.prototype.weakHas = function(key) {
var fullKey = this.composeKey(key);
return this.cacheKeys.hasOwnProperty(fullKey);
}

// If method:url is in localforage then call onHit(composedKey, response) else call onMiss(composedKey)
RequestCache.prototype.match = function(method, url, onHit, onMiss) {
var key = this.composeKey(method, url);
RequestCache.prototype.match = function(key, onHit, onMiss) {
var fullKey = this.composeKey(key);
var self = this;
this.store.getItem(key)
this.store.getItem(fullKey)
.then(function(cachedValue) {
try {
if (cachedValue === null) {
delete self.cacheKeys[key]; // Handle the case where something else managed to delete an entry from localforage
handleCacheMiss.call(self, key, onMiss);
delete self.cacheKeys[fullKey]; // Handle the case where something else managed to delete an entry from localforage
handleCacheMiss.call(self, fullKey, onMiss);
} else {
handleCacheHit.call(self, key, cachedValue, onHit);
handleCacheHit.call(self, fullKey, cachedValue, onHit);
}
} catch (err) {
// callback was source of the error, not this.store.getItem
console.error('[Teuthis] proxy-cache-match handler error ' + err);
console.error(err);
delete self.cacheKeys[key];
delete self.cacheKeys[fullKey];
}
}).catch(function(err) {
// care - this will catch errors in the handler, not just errors in the cache itself
Expand All @@ -197,22 +197,22 @@ RequestCache.prototype.match = function(method, url, onHit, onMiss) {
// Derived from xhr.send.apply(xhr, arguments); in send() inside the miss callback
console.error('[Teuthis] proxy-cache-match error ' + err);
console.error(err);
delete self.cacheKeys[key];
handleCacheMiss.call(self, key, onMiss);
delete self.cacheKeys[fullKey];
handleCacheMiss.call(self, fullKey, onMiss);
});
}

// Put value of response for method:url into localforage, and call done(), or call done(err) if an error happens
RequestCache.prototype.put = function(method, url, value, done) {
var key = this.composeKey(method, url);
if (this.options.debugCachePuts) console.log('[Teuthis] proxy-cache-put ' + key);
RequestCache.prototype.put = function(key, value, done) {
var fullKey = this.composeKey(key);
if (this.options.debugCachePuts) console.log('[Teuthis] proxy-cache-put ' + fullKey);
var self = this;
this.store.setItem(key, value).then(function() {
self.cacheKeys[key] = true;
this.store.setItem(fullKey, value).then(function() {
self.cacheKeys[fullKey] = true;

let m = 0;
// This is a phenomenally bad leaky abstraction...
var vd = _.has(v, 'v') ? v.v : v;
var vd = _.has(value, 'v') ? value.v : value;
if (typeof vd === 'string') m = vd.length;
else if (_.isArrayBuffer(vd)) m = vd.byteLength;
self.stats.memory += m;
Expand Down
134 changes: 97 additions & 37 deletions lib/xhr-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ var options = {
};

// Global function to determine if a request should be cached or not
var cacheSelector = function() { return false; }
var cacheSelector = function(requestInfo) { return false; }

var onerrorhook = function(e, isOnSend, xhr, realXhr, alternativeResponse) { }
var onloadhook = function(isOnSend, xhr, realXhr) { }
var onmisshook = function(xhr, realXhr, res) { return false; }
var cachekeymangler = function(urlkey) { return urlkey; }
var cachekeycomposer = function(requestInfo) { return requestInfo.method + '__' + requestInfo.path + "__" + requestInfo.params.toString(); }
var generateRequestInfo = function(method, url, body) {
const uri = new URL(url, window.location.origin);

let payload;
switch (method) {
case "GET":
payload = uri.search;
break;
case "POST":
payload = body;
break;
}

return {
uri: uri,
method: method,
host: uri.host.toLowerCase(),
path: uri.pathname.toLowerCase(),
payload: payload
};
}

var requestCache = null;

Expand All @@ -53,19 +74,29 @@ function XMLHttpRequestProxy() {

var self = this;

this.requestHeaders = undefined;
this.requestBody = undefined;

// Facade the status, statusText and response properties to spec XMLHttpRequest
this.status = 0; // This is the status if error due to browser offline, etc.
this.statusText = "";
this.response = "";
this.responseText = "";
this.responseHeaders = undefined;
// Facade the onload, onreadystatechange to spec XMLHttpRequest
this.onreadystatechange = null;
this.onload = null;

Object.defineProperty(self, 'proxymethod', { get: function() {return method_;} });
Object.defineProperty(self, 'proxyurl', { get: function() {return url_;} });
Object.defineProperty(self, 'response', { get: function() {
if (self.responseHeaders['content-type'] && self.responseHeaders['content-type'].includes("json")) {
return JSON.parse(self.responseText);
}
return self.responseText;
} });

function shouldCache(method, url) {
if (_.isFunction(cacheSelector)) { return cacheSelector.call(self, method, url); }
function shouldCache(method, url, body) {
if (_.isFunction(cacheSelector)) { return cacheSelector.call(self, generateRequestInfo(method, url, body)); }
return false;
}

Expand All @@ -85,18 +116,28 @@ function XMLHttpRequestProxy() {
if (options.debugEvents) console.log('[Teuthis] proxy-xhr-onload ' + xhr.status + ' ' + xhr.statusText);
self.status = xhr.status;
self.statusText = xhr.statusText;
self.response = xhr.response;
if (xhr.status >= 200 && xhr.status < 300 && xhr.response) {
self.responseText = xhr.responseText;
if (!self.responseHeaders) {
self.responseHeaders = xhr.getAllResponseHeaders();
if (self.responseHeaders) {
self.responseHeaders = self.responseHeaders.trim().split('\r\n').reduce((acc, current) => {
const [x,v] = current.split(': ');
return Object.assign(acc, { [x.toLowerCase()] : v });
}, {});
}
}

if (xhr.status >= 200 && xhr.status < 300 && xhr.responseText) {
if (shouldAddToCache_ === true) {
var mangled = cachekeymangler(url_);
if (options.debugEvents) console.log('[Teuthis] proxy-xhr-onload do-put ' + method_ + ' ' + mangled);
var cacheKey = cachekeycomposer(generateRequestInfo(method_, url_, self.requestBody));
if (options.debugEvents) console.log('[Teuthis] proxy-xhr-onload do-put ' + method_ + ' ' + cacheKey);
// console.log('proxy-cache-type ' + xhr.responseType); // + ', ' + xhr.responseText.substring(0,64));
// if (xhr.responseType === 'arraybuffer')
// Assuming the response is string or arraybuffer then clone it first, otherwise things seem to not work properly
var savedResponse = xhr.response.slice(0);
var cachedResponse = { v: savedResponse, ts: Date.now() };
var savedResponseText = xhr.responseText.slice(0);
var cachedResponse = { v: savedResponseText, ts: Date.now() };

store.put(method_, mangled, cachedResponse, function() {
store.put(cacheKey, cachedResponse, function() {
if (_.isFunction(onloadhook)) { onloadhook(shouldAddToCache_, self, xhr); }
if (_.isFunction(self.onload)) { self.onload(); }
});
Expand All @@ -122,8 +163,8 @@ function XMLHttpRequestProxy() {
self.statusText = '200 OK';
if (_.isFunction(self.onreadystatechange)) { self.onreadystatechange(); }

if (alternativeResponse.response) {
self.response = alternativeResponse.response;
if (alternativeResponse.responseText) {
self.responseText = alternativeResponse.responseText;
}
self.readyState = 4; // Done
if (_.isFunction(onloadhook)) { onloadhook('on-error', self, xhr); }
Expand All @@ -144,84 +185,103 @@ function XMLHttpRequestProxy() {
xhr.open.apply(xhr, arguments);
};

this.setRequestHeader = function(name, value) {
if (!this.requestHeaders) {
this.requestHeaders = {};
}
// collect headers
this.requestHeaders[name] = value;
xhr.setRequestHeader.apply(xhr, arguments);
};

// Facade XMLHttpRequest.send() with a version that queries our offline cache,
// calls the original if the response is not found in the cache, then adds the response to the cache,
// or calls to onload() with the cached response if found
this.send = function() {
const sendArguments = arguments;
this.requestBody = sendArguments[0];

if (options.debugMethods) console.log('[Teuthis] proxy-xhr-send ' + method_ + ' ' + url_);
if (shouldCache(method_, url_)) {
var mangled = cachekeymangler(url_);
if (options.debugCache) console.log('[Teuthis] proxy-try-cache ' + method_ + ' ' + mangled);
store.match(method_, mangled, function(key, cachedValue) {
if (shouldCache(method_, url_, this.requestBody)) {
var cacheKey = cachekeycomposer(generateRequestInfo(method_, url_, this.requestBody));
if (options.debugCache) console.log('[Teuthis] proxy-try-cache ' + method_ + ' ' + cacheKey);
store.match(cacheKey, handleHit, handleMiss);

function handleHit(key, cachedValue) {
if (!cachedValue.v || !cachedValue.ts) {
// something is badly wrong... we need to treat as a miss
console.warn('invalid cache data');
// This is a hack copy/paste - we really need to refactor this code
if (options.debugCache) console.log('[Teuthis] proxy-try-cache miss ' + method_ + ' ' + mangled);
if (options.debugCache) console.log('[Teuthis] proxy-try-cache miss ' + method_ + ' ' + cacheKey);
// miss - not in our cache. So try and fetch from the real Internet
//console.log('onMiss called'); console.log(arguments);
if (_.isFunction(onmisshook)) {
var res = {url: url_, status: +200, statusText: '200 OK', response: undefined, readyState: 4};
var res = {url: url_, status: +200, statusText: '200 OK', responseText: undefined, readyState: 4};
var patch = onmisshook(self, xhr, res);
if (patch) {
// Miss hook returns undefined, or otherwise, a replacement response,
// and it should fix self status, statusText, response, and readyState
self.status = res.status;
self.statusText = res.statusText;
if (_.isFunction(self.onreadystatechange)) { self.onreadystatechange(); }
self.response = res.response;
self.responseText = res.responseText;
self.readyState = res.readyState;
if (_.isFunction(onloadhook)) { onloadhook('on-match', self, xhr); }
if (_.isFunction(self.onload)) { self.onload(); }
return;
}
}
shouldAddToCache_ = true;
xhr.send.apply(xhr, arguments);
xhr.send.apply(xhr, sendArguments);
return;
}
if (options.debugCache) console.log('[Teuthis] proxy-try-cache hit ' + method_ + ' ' + mangled);

if (options.debugCache) console.log('[Teuthis] proxy-try-cache hit ' + method_ + ' ' + cacheKey);
// hit
self.status = +200;
self.statusText = '200 OK';
if (_.isFunction(self.onreadystatechange)) { self.onreadystatechange(); }
self.response = cachedValue.v;
self.responseText = cachedValue.v;
self.readyState = 4; // Done
if (_.isFunction(onloadhook)) { onloadhook('on-match', self, xhr); }
if (_.isFunction(self.onload)) { self.onload(); }

// Now, how do we update the LRU time?
var o = {v: cachedValue.v, ts: Date.now()};
store.put(method_, mangled, o, function() { });
}, function(key) {
if (options.debugCache) console.log('[Teuthis] proxy-try-cache miss ' + method_ + ' ' + mangled);
store.put(cacheKey, o, function() { });
}

function handleMiss(key) {
if (options.debugCache) console.log('[Teuthis] proxy-try-cache miss ' + method_ + ' ' + cacheKey);
// miss - not in our cache. So try and fetch from the real Internet
//console.log('onMiss called'); console.log(arguments);
if (_.isFunction(onmisshook)) {
var res = {url: url_, status: +200, statusText: '200 OK', response: undefined, readyState: 4};
var res = {url: url_, status: +200, statusText: '200 OK', responseText: undefined, readyState: 4};
var patch = onmisshook(self, xhr, res);
if (patch) {
// Miss hook returns undefined, or otherwise, a replacement response,
// and it should fix self status, statusText, response, and readyState
self.status = res.status;
self.statusText = res.statusText;
if (_.isFunction(self.onreadystatechange)) { self.onreadystatechange(); }
self.response = res.response;
self.responseText = res.responseText;
self.readyState = res.readyState;
if (_.isFunction(onloadhook)) { onloadhook('on-match', self, xhr); }
if (_.isFunction(self.onload)) { self.onload(); }
return;
}
}

shouldAddToCache_ = true;
xhr.send.apply(xhr, arguments);
});
xhr.send.apply(xhr, sendArguments);
}
} else {
xhr.send.apply(xhr, arguments);
xhr.send.apply(xhr, sendArguments);
}
};

// facade all other XMLHttpRequest getters, except 'status'
["responseURL", "responseText", "responseXML", "upload"].forEach(function(item) {
// facade all other XMLHttpRequest getters, except 'status' and 'response'
["responseURL", "responseXML", "upload"].forEach(function(item) {
Object.defineProperty(self, item, {
get: function() {return xhr[item];},
});
Expand All @@ -237,7 +297,7 @@ function XMLHttpRequestProxy() {

// facade all pure XMLHttpRequest methods and EVentTarget ancestor methods
["addEventListener", "removeEventListener", "dispatchEvent",
"abort", "getAllResponseHeaders", "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
"abort", "getAllResponseHeaders", "getResponseHeader", "overrideMimeType"].forEach(function(item) {
Object.defineProperty(self, item, {
value: function() {return xhr[item].apply(xhr, arguments);},
});
Expand Down Expand Up @@ -269,8 +329,8 @@ XMLHttpRequestProxy.setMissHook = function (onmisshook_) {
onmisshook = onmisshook_;
}

XMLHttpRequestProxy.setCacheKeyMangler = function (cachekeymangler_) {
cachekeymangler = cachekeymangler_;
XMLHttpRequestProxy.setCacheKeyComposer = function (cachekeycomposer_) {
cachekeycomposer = cachekeycomposer_;
}

// Get the underlying RequestCache store so the user can monitor usage statistics, etc.
Expand Down