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

stale-while-revalidate implementation update #66

Merged
merged 14 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
130 changes: 96 additions & 34 deletions lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,40 +115,38 @@ function Cache(options) {

// mapping from hash of request info to time ordered list or responses
this._requestInfoToResponses = new Dict();
this._requestInfoRevalidating = new Dict();
}

var cp = Cache.prototype;

function getExpireTime(response, cacheControlHeaders, cacheDirectives) {
cp.setDebug = function (debug) {
this._debug = debug;
};

function getExpireTime(response, cacheDirectives, revalidating) {

var maxAgeAt = -1,
staleButRevalidateAt = -1;

if (cacheControlHeaders) {

var date = response.headers.date;
if (!date) {
// TODO RFC error here, maybe we should ignore the request
date = new Date().getTime();
} else {
date = new Date(date).getTime();
}
var date = response.headers.date;
if (!date) {
// TODO RFC error here, maybe we should ignore the request
date = new Date().getTime();
} else {
date = new Date(date).getTime();
}

if (cacheDirectives['max-age']) {
maxAgeAt = date + (cacheDirectives['max-age'] * 1000);
}
if (cacheDirectives['stale-while-revalidate']) {
staleButRevalidateAt = date + (cacheDirectives['stale-while-revalidate'] * 1000);
}
if (cacheDirectives['max-age']) {
maxAgeAt = date + (cacheDirectives['max-age'] * 1000);
}

return maxAgeAt > staleButRevalidateAt ? maxAgeAt : staleButRevalidateAt;
}
if (revalidating && cacheDirectives['stale-while-revalidate']) {
staleButRevalidateAt = date + (cacheDirectives['stale-while-revalidate'] * 1000);
}

function isExpired(candidate, currentTime) {
var cacheControlHeaders = getCacheControlHeaders(candidate);
var cacheResponseDirectives = parseCacheControl(cacheControlHeaders);
return getExpireTime(candidate, cacheControlHeaders, cacheResponseDirectives) < currentTime;
return maxAgeAt > staleButRevalidateAt ? maxAgeAt : staleButRevalidateAt;
}

var privateCacheHashHeaders = ['authorization'];
Expand All @@ -170,12 +168,13 @@ function getResponseHash(requestInfo, cacheResponseDirectives) {
return (requestInfo.hash + hash);
}

function satisfiesRequest(requestInfo, candidate, candidateHash) {
function satisfiesRequest(requestInfo, candidate, candidateHash, revalidating) {

var currentTime = new Date().getTime(), // TODO get time from requestInfo ?
cacheControlHeaders = getCacheControlHeaders(candidate),
cacheResponseDirectives = parseCacheControl(cacheControlHeaders);

return getExpireTime(candidate, cacheControlHeaders, cacheResponseDirectives) > currentTime &&
return getExpireTime(candidate, cacheResponseDirectives, revalidating) > currentTime &&
getResponseHash(requestInfo, cacheResponseDirectives) === candidateHash;
}

Expand Down Expand Up @@ -207,7 +206,9 @@ cp.match = function (requestInfo/*, options*/) {
if (!(requestInfo instanceof RequestInfo)) {
reject(new TypeError("Invalid requestInfo argument"));
} else {
var requestCacheDirectives = requestInfo.cacheDirectives;

var requestCacheDirectives = requestInfo.cacheDirectives,
requestIsRevalidating = self.isRevalidating(requestInfo);

if (
// Cache by default
Expand All @@ -223,38 +224,103 @@ cp.match = function (requestInfo/*, options*/) {

while (
response === null &&
(candidateHash = cadidatesHashs.next().value)
(candidateHash = cadidatesHashs.next()) &&
(candidateHash.done === false)
) {
candidate = candidates.get(candidateHash);

candidate = candidateHash.value &&
candidates.get(candidateHash.value);

if (
candidate &&
satisfiesRequest(requestInfo, candidate, candidateHash)
satisfiesRequest(requestInfo, candidate, candidateHash.value, requestIsRevalidating)
) {
response = candidate;
}
}
}

resolve(response);
}
});
};

function isExpired(candidate, currentTime) {
var cacheControlHeaders = getCacheControlHeaders(candidate),
cacheResponseDirectives = parseCacheControl(cacheControlHeaders);
return getExpireTime(candidate, cacheResponseDirectives) < currentTime;
}

cp._expireOld = function () {
var self = this;
self._requestInfoToResponses.forEach(function (candidates) {
candidates.forEach(function (response, request) {
candidates.forEach(function (response, requestInfoHash) {
var currentTime = new Date().getTime();
if (isExpired(response, currentTime)) {
if (self._debug) {
self._log.debug("Evicted Response from cache for: " + request);
self._log.debug("Evicted Response from cache for requestInfo Hash: " + requestInfoHash);
}
self._requestInfoToResponses.delete(request);
self._requestInfoToResponses.delete(requestInfoHash);
}
});
});
};

/**
* Check if a given requestInfo is currently revalidating.
*/
cp.isRevalidating = function (requestInfo) {
return !!this._requestInfoRevalidating.get(requestInfo.hash);
};

/**
* Add given requestInfo to revalidating.
*/
cp.revalidate = function (requestInfo) {
var self = this,
revalidate = self._requestInfoRevalidating.get(requestInfo.hash) || 0;

// TODO special behavior ?
if (self._requestInfoRevalidating.get(requestInfo.hash)) {
if (self._debug) {
self._log.debug("Updated requestInfo for revalidating with hash: " + requestInfo.hash);
}
} else {
if (self._debug) {
self._log.debug("Added requestInfo to revalidate with hash: " + requestInfo.hash);
}
}
revalidate++;

self._requestInfoRevalidating.set(requestInfo.hash, revalidate);
};

/**
* Clear given requestInfo from revalidation.
*/
cp.validated = function (requestInfo) {
var self = this,
revalidate = self._requestInfoRevalidating.get(requestInfo.hash) || 0;

if (revalidate > 0) {

revalidate--

if (self._debug) {
self._log.debug("Evicted requestInfo from revalidate with hash: " + requestInfo.hash);
}

if (revalidate > 0) {
self._requestInfoRevalidating.set(requestInfo.hash, revalidate);
} else {
self._requestInfoRevalidating.delete(requestInfo.hash);
}
}
};

/**
* Check requestInfo response for cache update, then update cache if cachable.
*/
cp.put = function (requestInfo, response) {
var self = this;
return new Promise(function (resolve, reject) {
Expand Down Expand Up @@ -286,10 +352,6 @@ cp.put = function (requestInfo, response) {
});
};

cp.setDebug = function (debug) {
this._debug = debug;
};

//////////////////////////////////////////// Exports ////////////////////////////////////////////
module.exports = {
RequestInfo: RequestInfo,
Expand Down
26 changes: 17 additions & 9 deletions lib/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ function Configuration(options) {
self.agent = new Agent({
log: self._log
});


}

util.inherits(Configuration, EventEmitter);
Expand Down Expand Up @@ -118,35 +116,45 @@ confProto.getTransportUrl = function (url) {
};

confProto.onPush = function (pushRequest) {
var self = this;
var origin = getOrigin(pushRequest.scheme + "://" + pushRequest.headers.host);
var self = this,
cache = self.cache,
origin = getOrigin(pushRequest.scheme + "://" + pushRequest.headers.host);

// Create href
pushRequest.href = origin + pushRequest.url;

if (self.debug) {
self._log.info("Received push promise for: " + pushRequest.href);
}

var requestInfo = new RequestInfo(pushRequest.method, pushRequest.href, pushRequest.headers);

// Set requestInfo revalidate state
// TODO pass pushRequest for future dedup pending requestInfo
cache.revalidate(requestInfo);

pushRequest.on('response', function (response) {

// TODO match or partial move to _sendViaHttp2 xhr.js to avoid maintain both
response.on('data', function (data) {
response.data = mergeTypedArrays(response.data, data);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to clean on L130

var requestInfo = origin ? new RequestInfo(pushRequest.method, origin + pushRequest.url, pushRequest.headers) :
new RequestInfo(pushRequest.method, getOrigin(pushRequest) + pushRequest.url, pushRequest.headers);

response.on('end', function () {
self.cache.put(requestInfo, response).then(function () {
cache.put(requestInfo, response).then(function () {
self._log.debug("Cache updated via push for proxied XHR(" + pushRequest.href + ")");
}, function (cacheError) {
self._log.debug("Cache error via push for proxied XHR(" + pushRequest.href + "):" + cacheError.message);
}).then(function () {
// Clear requestInfo revalidate state
cache.validated(requestInfo);
});
});

response.on('error', function (e) {
self._log.warn("Server push stream error: " + e);
});
});

};

// open h2 pull channel
Expand Down
46 changes: 21 additions & 25 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,45 @@ var parseUrl = function (href) {
};

var redefine = function (obj, prop, value) {

if (obj[prop]) {
// TODO, consider erasing scope/hiding (enumerable: false)
obj["_" + prop] = obj[prop];
}

Object.defineProperty(obj, prop, {
enumerable: obj.propertyIsEnumerable(prop),
value: value,
enumerable: obj.propertyIsEnumerable(prop),
configurable: true
});
};

var defineGetter = function (obj, prop, getter) {

if (obj[prop]) {
// TODO, consider erasing scope/hiding (enumerable: false)
obj["_" + prop] = obj[prop];
}

Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get: getter
});
};

var definePrivate = function (obj, prop, value) {
Object.defineProperty(obj, prop, {
enumerable: false,
value: value,
enumerable: false,
configurable: true
});
};

var definePublic = function (obj, prop, value) {
Object.defineProperty(obj, prop, {
enumerable: true,
value: value,
enumerable: true,
configurable: true
});
};
Expand Down Expand Up @@ -174,26 +190,6 @@ var mergeTypedArrays = function (a, b) {
return c;
};

var memoize = function(func) {
var stringifyJson = JSON.stringify,
cache = {};

var cachedfun = function() {
var hash = stringifyJson(arguments);
return (hash in cache) ? cache[hash] : cache[hash] = func.apply(this, arguments);
};

cachedfun.__cache = (function() {
cache.remove = cache.remove || function() {
var hash = stringifyJson(arguments);
return (delete cache[hash]);
};
return cache;
}).call(this);

return cachedfun;
};

var dataToType = function (data, type) {
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
switch (type) {
Expand Down Expand Up @@ -254,6 +250,7 @@ var serializeXhrBody = function (xhrInfo, body) {
module.exports = {
parseUrl: parseUrl,
redefine: redefine,
defineGetter: defineGetter,
definePrivate: definePrivate,
definePublic: definePublic,
resolvePort: resolvePort,
Expand All @@ -263,8 +260,7 @@ module.exports = {
serializeXhrBody: serializeXhrBody,
caseInsensitiveEquals: caseInsensitiveEquals,
mergeTypedArrays: mergeTypedArrays,
Utf8ArrayToStr: Utf8ArrayToStr,
memoize: memoize
Utf8ArrayToStr: Utf8ArrayToStr
};


Loading