Clean up gr-rest-api-interface's caching
Replace a shared object with an instance of a class that wraps Map.
The new cache implementation takes window.CANONICAL_PATH into account.
If PolyGerrit is embedded in an environment where some elements are used
before CANONICAL_PATH is set, they would potentially poison the cache.
Change-Id: I43b0d988f2e371caab48934ee101f7d7e723037d
(cherry picked from commit 24a01b06f0
)
This commit is contained in:

committed by
David Pursehouse

parent
8fc534661e
commit
35efa269fa
@@ -135,6 +135,42 @@
|
||||
const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
|
||||
'/revisions/*';
|
||||
|
||||
/**
|
||||
* Wrapper around Map for caching server responses. Site-based so that
|
||||
* changes to CANONICAL_PATH will result in a different cache going into
|
||||
* effect.
|
||||
*/
|
||||
class SiteBasedCache {
|
||||
constructor() {
|
||||
// Container of per-canonical-path caches.
|
||||
this._data = new Map();
|
||||
}
|
||||
|
||||
// Returns the cache for the current canonical path.
|
||||
_cache() {
|
||||
if (!this._data.has(window.CANONICAL_PATH)) {
|
||||
this._data.set(window.CANONICAL_PATH, new Map());
|
||||
}
|
||||
return this._data.get(window.CANONICAL_PATH);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this._cache().has(key);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this._cache().get(key);
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this._cache().set(key, value);
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this._cache().delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Polymer({
|
||||
is: 'gr-rest-api-interface',
|
||||
|
||||
@@ -171,7 +207,7 @@
|
||||
properties: {
|
||||
_cache: {
|
||||
type: Object,
|
||||
value: {}, // Intentional to share the object across instances.
|
||||
value: new SiteBasedCache(), // Shared across instances.
|
||||
},
|
||||
_credentialCheck: {
|
||||
type: Object,
|
||||
@@ -268,7 +304,7 @@
|
||||
}
|
||||
return res;
|
||||
}).catch(err => {
|
||||
const isLoggedIn = !!this._cache['/accounts/self/detail'];
|
||||
const isLoggedIn = !!this._cache.get('/accounts/self/detail');
|
||||
if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
|
||||
this.checkCredentials();
|
||||
return;
|
||||
@@ -784,7 +820,7 @@
|
||||
*/
|
||||
saveDiffPreferences(prefs, opt_errFn) {
|
||||
// Invalidate the cache.
|
||||
this._cache['/accounts/self/preferences.diff'] = undefined;
|
||||
this._cache.delete('/accounts/self/preferences.diff');
|
||||
return this._send({
|
||||
method: 'PUT',
|
||||
url: '/accounts/self/preferences.diff',
|
||||
@@ -800,7 +836,7 @@
|
||||
*/
|
||||
saveEditPreferences(prefs, opt_errFn) {
|
||||
// Invalidate the cache.
|
||||
this._cache['/accounts/self/preferences.edit'] = undefined;
|
||||
this._cache.delete('/accounts/self/preferences.edit');
|
||||
return this._send({
|
||||
method: 'PUT',
|
||||
url: '/accounts/self/preferences.edit',
|
||||
@@ -816,7 +852,7 @@
|
||||
reportUrlAsIs: true,
|
||||
errFn: resp => {
|
||||
if (!resp || resp.status === 403) {
|
||||
this._cache['/accounts/self/detail'] = null;
|
||||
this._cache.delete('/accounts/self/detail');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -828,7 +864,7 @@
|
||||
reportUrlAsIs: true,
|
||||
errFn: resp => {
|
||||
if (!resp || resp.status === 403) {
|
||||
this._cache['/accounts/self/avatar.change.url'] = null;
|
||||
this._cache.delete('/accounts/self/avatar.change.url');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -910,7 +946,7 @@
|
||||
return this._send(req).then(() => {
|
||||
// If result of getAccountEmails is in cache, update it in the cache
|
||||
// so we don't have to invalidate it.
|
||||
const cachedEmails = this._cache['/accounts/self/emails'];
|
||||
const cachedEmails = this._cache.get('/accounts/self/emails');
|
||||
if (cachedEmails) {
|
||||
const emails = cachedEmails.map(entry => {
|
||||
if (entry.email === email) {
|
||||
@@ -919,7 +955,7 @@
|
||||
return {email};
|
||||
}
|
||||
});
|
||||
this._cache['/accounts/self/emails'] = emails;
|
||||
this._cache.set('/accounts/self/emails', emails);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -930,11 +966,11 @@
|
||||
_updateCachedAccount(obj) {
|
||||
// If result of getAccount is in cache, update it in the cache
|
||||
// so we don't have to invalidate it.
|
||||
const cachedAccount = this._cache['/accounts/self/detail'];
|
||||
const cachedAccount = this._cache.get('/accounts/self/detail');
|
||||
if (cachedAccount) {
|
||||
// Replace object in cache with new object to force UI updates.
|
||||
this._cache['/accounts/self/detail'] =
|
||||
Object.assign({}, cachedAccount, obj);
|
||||
this._cache.set('/accounts/self/detail',
|
||||
Object.assign({}, cachedAccount, obj));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1064,14 +1100,14 @@
|
||||
if (!res) { return; }
|
||||
if (res.status === 403) {
|
||||
this.fire('auth-error');
|
||||
this._cache['/accounts/self/detail'] = null;
|
||||
this._cache.delete('/accounts/self/detail');
|
||||
} else if (res.ok) {
|
||||
return this.getResponseObject(res);
|
||||
}
|
||||
}).then(res => {
|
||||
this._credentialCheck.checking = false;
|
||||
if (res) {
|
||||
this._cache['/accounts/self/detail'] = res;
|
||||
this._cache.delete('/accounts/self/detail');
|
||||
}
|
||||
return res;
|
||||
}).catch(err => {
|
||||
@@ -1154,13 +1190,13 @@
|
||||
return this._sharedFetchPromises[req.url];
|
||||
}
|
||||
// TODO(andybons): Periodic cache invalidation.
|
||||
if (this._cache[req.url] !== undefined) {
|
||||
return Promise.resolve(this._cache[req.url]);
|
||||
if (this._cache.has(req.url)) {
|
||||
return Promise.resolve(this._cache.get(req.url));
|
||||
}
|
||||
this._sharedFetchPromises[req.url] = this._fetchJSON(req)
|
||||
.then(response => {
|
||||
if (response !== undefined) {
|
||||
this._cache[req.url] = response;
|
||||
this._cache.set(req.url, response);
|
||||
}
|
||||
this._sharedFetchPromises[req.url] = undefined;
|
||||
return response;
|
||||
|
@@ -38,11 +38,15 @@ limitations under the License.
|
||||
suite('gr-rest-api-interface tests', () => {
|
||||
let element;
|
||||
let sandbox;
|
||||
let ctr = 0;
|
||||
|
||||
setup(() => {
|
||||
// Modify CANONICAL_PATH to effectively reset cache.
|
||||
ctr += 1;
|
||||
window.CANONICAL_PATH = `test${ctr}`;
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
element = fixture('basic');
|
||||
element._cache = {};
|
||||
element._projectLookup = {};
|
||||
const testJSON = ')]}\'\n{"hello": "bonjour"}';
|
||||
sandbox.stub(window, 'fetch').returns(Promise.resolve({
|
||||
@@ -85,7 +89,7 @@ limitations under the License.
|
||||
|
||||
test('cached promise', done => {
|
||||
const promise = Promise.reject('foo');
|
||||
element._cache['/foo'] = promise;
|
||||
element._cache.set('/foo', promise);
|
||||
element._fetchSharedCacheURL({url: '/foo'}).catch(p => {
|
||||
assert.equal(p, 'foo');
|
||||
done();
|
||||
@@ -98,19 +102,20 @@ limitations under the License.
|
||||
gr: 'guten tag',
|
||||
noval: null,
|
||||
});
|
||||
assert.equal(url, '/path/?sp=hola&gr=guten%20tag&noval');
|
||||
assert.equal(url,
|
||||
window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
|
||||
|
||||
url = element._urlWithParams('/path/', {
|
||||
sp: 'hola',
|
||||
en: ['hey', 'hi'],
|
||||
});
|
||||
assert.equal(url, '/path/?sp=hola&en=hey&en=hi');
|
||||
assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
|
||||
|
||||
// Order must be maintained with array params.
|
||||
url = element._urlWithParams('/path/', {
|
||||
l: ['c', 'b', 'a'],
|
||||
});
|
||||
assert.equal(url, '/path/?l=c&l=b&l=a');
|
||||
assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
|
||||
});
|
||||
|
||||
test('request callbacks can be canceled', done => {
|
||||
@@ -441,7 +446,7 @@ limitations under the License.
|
||||
Promise.reject({message: 'Failed to fetch'}));
|
||||
window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
|
||||
// Emulate logged in.
|
||||
element._cache['/accounts/self/detail'] = {};
|
||||
element._cache.set('/accounts/self/detail', {});
|
||||
const serverErrorStub = sandbox.stub();
|
||||
element.addEventListener('server-error', serverErrorStub);
|
||||
const authErrorStub = sandbox.stub();
|
||||
@@ -450,7 +455,7 @@ limitations under the License.
|
||||
flush(() => {
|
||||
assert.isTrue(authErrorStub.called);
|
||||
assert.isFalse(serverErrorStub.called);
|
||||
assert.isNull(element._cache['/accounts/self/detail']);
|
||||
assert.isFalse(element._cache.has('/accounts/self/detail'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -471,7 +476,7 @@ limitations under the License.
|
||||
];
|
||||
window.fetch.restore();
|
||||
sandbox.stub(window, 'fetch', url => {
|
||||
if (url === '/accounts/self/detail') {
|
||||
if (url === window.CANONICAL_PATH + '/accounts/self/detail') {
|
||||
return Promise.resolve(responses.shift());
|
||||
}
|
||||
});
|
||||
@@ -487,7 +492,7 @@ limitations under the License.
|
||||
|
||||
test('checkCredentials promise rejection', () => {
|
||||
window.fetch.restore();
|
||||
element._cache['/accounts/self/detail'] = true;
|
||||
element._cache.set('/accounts/self/detail', true);
|
||||
sandbox.spy(element, 'checkCredentials');
|
||||
sandbox.stub(window, 'fetch', url => {
|
||||
return Promise.reject({message: 'Failed to fetch'});
|
||||
@@ -515,10 +520,10 @@ limitations under the License.
|
||||
test('saveDiffPreferences invalidates cache line', () => {
|
||||
const cacheKey = '/accounts/self/preferences.diff';
|
||||
sandbox.stub(element, '_send');
|
||||
element._cache[cacheKey] = {tab_size: 4};
|
||||
element._cache.set(cacheKey, {tab_size: 4});
|
||||
element.saveDiffPreferences({tab_size: 8});
|
||||
assert.isTrue(element._send.called);
|
||||
assert.notOk(element._cache[cacheKey]);
|
||||
assert.isFalse(element._cache.has(cacheKey));
|
||||
});
|
||||
|
||||
test('getAccount when resp is null does not add anything to the cache',
|
||||
@@ -529,11 +534,11 @@ limitations under the License.
|
||||
|
||||
element.getAccount().then(() => {
|
||||
assert.isTrue(element._fetchSharedCacheURL.called);
|
||||
assert.isNull(element._cache[cacheKey]);
|
||||
assert.isFalse(element._cache.has(cacheKey));
|
||||
done();
|
||||
});
|
||||
|
||||
element._cache[cacheKey] = 'fake cache';
|
||||
element._cache.set(cacheKey, 'fake cache');
|
||||
stub.lastCall.args[0].errFn();
|
||||
});
|
||||
|
||||
@@ -545,10 +550,10 @@ limitations under the License.
|
||||
|
||||
element.getAccount().then(() => {
|
||||
assert.isTrue(element._fetchSharedCacheURL.called);
|
||||
assert.isNull(element._cache[cacheKey]);
|
||||
assert.isFalse(element._cache.has(cacheKey));
|
||||
done();
|
||||
});
|
||||
element._cache[cacheKey] = 'fake cache';
|
||||
element._cache.set(cacheKey, 'fake cache');
|
||||
stub.lastCall.args[0].errFn({status: 403});
|
||||
});
|
||||
|
||||
@@ -559,10 +564,10 @@ limitations under the License.
|
||||
|
||||
element.getAccount().then(response => {
|
||||
assert.isTrue(element._fetchSharedCacheURL.called);
|
||||
assert.equal(element._cache[cacheKey], 'fake cache');
|
||||
assert.equal(element._cache.get(cacheKey), 'fake cache');
|
||||
done();
|
||||
});
|
||||
element._cache[cacheKey] = 'fake cache';
|
||||
element._cache.set(cacheKey, 'fake cache');
|
||||
|
||||
stub.lastCall.args[0].errFn({});
|
||||
});
|
||||
@@ -728,7 +733,7 @@ limitations under the License.
|
||||
|
||||
test('setAccountStatus', () => {
|
||||
sandbox.stub(element, '_send').returns(Promise.resolve('OOO'));
|
||||
element._cache['/accounts/self/detail'] = {};
|
||||
element._cache.set('/accounts/self/detail', {});
|
||||
return element.setAccountStatus('OOO').then(() => {
|
||||
assert.isTrue(element._send.calledOnce);
|
||||
assert.equal(element._send.lastCall.args[0].method, 'PUT');
|
||||
@@ -736,7 +741,7 @@ limitations under the License.
|
||||
'/accounts/self/status');
|
||||
assert.deepEqual(element._send.lastCall.args[0].body,
|
||||
{status: 'OOO'});
|
||||
assert.deepEqual(element._cache['/accounts/self/detail'],
|
||||
assert.deepEqual(element._cache.get('/accounts/self/detail'),
|
||||
{status: 'OOO'});
|
||||
});
|
||||
});
|
||||
@@ -830,7 +835,7 @@ limitations under the License.
|
||||
Promise.resolve([change_num, file_name, file_contents]));
|
||||
sandbox.stub(element, 'getResponseObject')
|
||||
.returns(Promise.resolve([change_num, file_name, file_contents]));
|
||||
element._cache['/changes/' + change_num + '/edit/' + file_name] = {};
|
||||
element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
|
||||
return element.saveChangeEdit(change_num, file_name, file_contents)
|
||||
.then(() => {
|
||||
assert.isTrue(element._send.calledOnce);
|
||||
@@ -849,7 +854,7 @@ limitations under the License.
|
||||
Promise.resolve([change_num, message]));
|
||||
sandbox.stub(element, 'getResponseObject')
|
||||
.returns(Promise.resolve([change_num, message]));
|
||||
element._cache['/changes/' + change_num + '/message'] = {};
|
||||
element._cache.set('/changes/' + change_num + '/message', {});
|
||||
return element.putChangeCommitMessage(change_num, message).then(() => {
|
||||
assert.isTrue(element._send.calledOnce);
|
||||
assert.equal(element._send.lastCall.args[0].method, 'PUT');
|
||||
@@ -1031,7 +1036,8 @@ limitations under the License.
|
||||
const changeNum = 4321;
|
||||
element._projectLookup[changeNum] = 'test';
|
||||
const params = {foo: 'bar'};
|
||||
const expectedUrl = '/changes/test~4321/detail?foo=bar';
|
||||
const expectedUrl =
|
||||
window.CANONICAL_PATH + '/changes/test~4321/detail?foo=bar';
|
||||
sandbox.stub(element._etags, 'getOptions');
|
||||
sandbox.stub(element._etags, 'collect');
|
||||
return element._getChangeDetail(changeNum, params).then(() => {
|
||||
|
Reference in New Issue
Block a user