Use ETag and If-None-Match for GET /change/detail
This prevents regenerating JSON response if nothing changed. Feature: Issue 6639 Change-Id: I1d542928d7b48a049cd4328f1a2ac1dcd1017cd6
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2017 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||||
|
|
||||||
|
<dom-module id="gr-etag-decorator">
|
||||||
|
<script src="gr-etag-decorator.js"></script>
|
||||||
|
</dom-module>
|
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (C) 2017 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
(function(window) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Prevent redefinition.
|
||||||
|
if (window.GrEtagDecorator) { return; }
|
||||||
|
|
||||||
|
// Limit cache size because /change/detail responses may be large.
|
||||||
|
const MAX_CACHE_SIZE = 30;
|
||||||
|
|
||||||
|
function GrEtagDecorator() {
|
||||||
|
this._etags = new Map();
|
||||||
|
this._payloadCache = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
GrEtagDecorator.prototype.getOptions = function(url, opt_options) {
|
||||||
|
const etag = this._etags.get(url);
|
||||||
|
if (!etag) {
|
||||||
|
return opt_options;
|
||||||
|
}
|
||||||
|
const options = Object.assign({}, opt_options);
|
||||||
|
options.headers = options.headers || new Headers();
|
||||||
|
options.headers.set('If-None-Match', this._etags.get(url));
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
GrEtagDecorator.prototype.collect = function(url, response, payload) {
|
||||||
|
if (!response ||
|
||||||
|
!response.ok ||
|
||||||
|
response.status !== 200 ||
|
||||||
|
response.status === 304) {
|
||||||
|
// 304 Not Modified means etag is still valid.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._payloadCache.set(url, payload);
|
||||||
|
const etag = response.headers && response.headers.get('etag');
|
||||||
|
if (!etag) {
|
||||||
|
this._etags.delete(url);
|
||||||
|
} else {
|
||||||
|
this._etags.set(url, etag);
|
||||||
|
this._trunkateCache();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GrEtagDecorator.prototype.getCachedPayload = function(url) {
|
||||||
|
return this._payloadCache.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
GrEtagDecorator.prototype._trunkateCache = function() {
|
||||||
|
for (const url of this._etags.keys()) {
|
||||||
|
if (this._etags.size <= MAX_CACHE_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._etags.delete(url);
|
||||||
|
this._payloadCache.delete(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.GrEtagDecorator = GrEtagDecorator;
|
||||||
|
})(window);
|
@@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2017 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||||
|
<title>gr-etag-decorator</title>
|
||||||
|
|
||||||
|
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||||
|
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||||
|
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||||
|
|
||||||
|
<script src="gr-etag-decorator.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
suite('gr-etag-decorator', () => {
|
||||||
|
let etag;
|
||||||
|
let sandbox;
|
||||||
|
|
||||||
|
const fakeRequest = (opt_etag, opt_status) => {
|
||||||
|
const headers = new Headers();
|
||||||
|
if (opt_etag) {
|
||||||
|
headers.set('etag', opt_etag);
|
||||||
|
}
|
||||||
|
const status = opt_status || 200;
|
||||||
|
return {ok: true, status, headers};
|
||||||
|
};
|
||||||
|
|
||||||
|
setup(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
etag = new GrEtagDecorator();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exists', () => {
|
||||||
|
assert.isOk(etag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works', () => {
|
||||||
|
etag.collect('/foo', fakeRequest('bar'));
|
||||||
|
const options = etag.getOptions('/foo');
|
||||||
|
assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates etags', () => {
|
||||||
|
etag.collect('/foo', fakeRequest('bar'));
|
||||||
|
etag.collect('/foo', fakeRequest('baz'));
|
||||||
|
const options = etag.getOptions('/foo');
|
||||||
|
assert.strictEqual(options.headers.get('If-None-Match'), 'baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('discards empty etags', () => {
|
||||||
|
etag.collect('/foo', fakeRequest('bar'));
|
||||||
|
etag.collect('/foo', fakeRequest());
|
||||||
|
const options = etag.getOptions('/foo', {headers: new Headers()});
|
||||||
|
assert.isNull(options.headers.get('If-None-Match'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('discards etags in order used', () => {
|
||||||
|
etag.collect('/foo', fakeRequest('bar'));
|
||||||
|
_.times(29, i => {
|
||||||
|
etag.collect('/qaz/' + i, fakeRequest('qaz'));
|
||||||
|
});
|
||||||
|
let options = etag.getOptions('/foo');
|
||||||
|
assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
|
||||||
|
etag.collect('/zaq', fakeRequest('zaq'));
|
||||||
|
options = etag.getOptions('/foo', {headers: new Headers()});
|
||||||
|
assert.isNull(options.headers.get('If-None-Match'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getCachedPayload', () => {
|
||||||
|
const payload = {};
|
||||||
|
etag.collect('/foo', fakeRequest('bar'), payload);
|
||||||
|
assert.strictEqual(etag.getCachedPayload('/foo'), payload);
|
||||||
|
etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
|
||||||
|
assert.strictEqual(etag.getCachedPayload('/foo'), payload);
|
||||||
|
etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
|
||||||
|
assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
@@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
|
|
||||||
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
|
|
||||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||||
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
|
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
|
||||||
|
<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
|
||||||
|
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
|
||||||
|
<link rel="import" href="gr-etag-decorator.html">
|
||||||
|
|
||||||
<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
|
<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
|
||||||
<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
|
<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
|
||||||
<script src="../../../bower_components/fetch/fetch.js"></script>
|
<script src="../../../bower_components/fetch/fetch.js"></script>
|
||||||
|
@@ -28,6 +28,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let auth = null;
|
let auth = null;
|
||||||
|
const etags = new GrEtagDecorator();
|
||||||
|
|
||||||
Polymer({
|
Polymer({
|
||||||
is: 'gr-rest-api-interface',
|
is: 'gr-rest-api-interface',
|
||||||
@@ -74,24 +75,26 @@
|
|||||||
auth = window.USE_GAPI_AUTH ? new GrGapiAuth() : new GrGerritAuth();
|
auth = window.USE_GAPI_AUTH ? new GrGapiAuth() : new GrGerritAuth();
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_options) {
|
/**
|
||||||
|
* Fetch JSON from url provided.
|
||||||
|
* Returns a Promise that resolves to a native Response.
|
||||||
|
* Doesn't do error checking. Supports cancel condition. Performs auth.
|
||||||
|
* Validates auth expiry errors.
|
||||||
|
* @param {string} url
|
||||||
|
* @param {function(response, error)} opt_errFn
|
||||||
|
* @param {function()} opt_cancelCondition
|
||||||
|
* @param {Object=} opt_params URL params, key-value hash.
|
||||||
|
* @param {Object=} opt_options Fetch options.
|
||||||
|
*/
|
||||||
|
_fetchRawJSON(url, opt_errFn, opt_cancelCondition, opt_params,
|
||||||
|
opt_options) {
|
||||||
const urlWithParams = this._urlWithParams(url, opt_params);
|
const urlWithParams = this._urlWithParams(url, opt_params);
|
||||||
return auth.fetch(urlWithParams, opt_options).then(response => {
|
return auth.fetch(urlWithParams, opt_options).then(response => {
|
||||||
if (opt_cancelCondition && opt_cancelCondition()) {
|
if (opt_cancelCondition && opt_cancelCondition()) {
|
||||||
response.body.cancel();
|
response.body.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
return response;
|
||||||
if (!response.ok) {
|
|
||||||
if (opt_errFn) {
|
|
||||||
opt_errFn.call(null, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.fire('server-error', {response});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getResponseObject(response);
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (opt_errFn) {
|
if (opt_errFn) {
|
||||||
opt_errFn.call(null, null, err);
|
opt_errFn.call(null, null, err);
|
||||||
@@ -102,6 +105,35 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch JSON from url provided.
|
||||||
|
* Returns a Promise that resolves to a parsed response.
|
||||||
|
* Same as {@link _fetchRawJSON}, plus error handling.
|
||||||
|
* @param {string} url
|
||||||
|
* @param {function(response, error)} opt_errFn
|
||||||
|
* @param {function()} opt_cancelCondition
|
||||||
|
* @param {Object=} opt_params URL params, key-value hash.
|
||||||
|
* @param {Object=} opt_options Fetch options.
|
||||||
|
*/
|
||||||
|
fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_options) {
|
||||||
|
return this._fetchRawJSON(
|
||||||
|
url, opt_errFn, opt_cancelCondition, opt_params, opt_options)
|
||||||
|
.then(response => {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
if (opt_errFn) {
|
||||||
|
opt_errFn.call(null, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fire('server-error', {response});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return response && this.getResponseObject(response);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_checkAuthRedirect() {
|
_checkAuthRedirect() {
|
||||||
const loggedIn = !!this._cache['/accounts/self/detail'];
|
const loggedIn = !!this._cache['/accounts/self/detail'];
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
@@ -462,20 +494,34 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
|
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
|
||||||
const options = this.listChangesOptionsToHex(
|
const params = this.listChangesOptionsToHex(
|
||||||
this.ListChangesOption.ALL_REVISIONS
|
this.ListChangesOption.ALL_REVISIONS
|
||||||
);
|
);
|
||||||
return this._getChangeDetail(changeNum, options, opt_errFn,
|
return this._getChangeDetail(changeNum, params, opt_errFn,
|
||||||
opt_cancelCondition);
|
opt_cancelCondition);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getChangeDetail(changeNum, options, opt_errFn,
|
_getChangeDetail(changeNum, params, opt_errFn,
|
||||||
opt_cancelCondition) {
|
opt_cancelCondition) {
|
||||||
return this.fetchJSON(
|
const url = this.getChangeActionURL(changeNum, null, '/detail');
|
||||||
this.getChangeActionURL(changeNum, null, '/detail'),
|
return this._fetchRawJSON(
|
||||||
|
url,
|
||||||
opt_errFn,
|
opt_errFn,
|
||||||
opt_cancelCondition,
|
opt_cancelCondition,
|
||||||
{O: options});
|
{O: params},
|
||||||
|
etags.getOptions(url))
|
||||||
|
.then(response => {
|
||||||
|
if (response && response.status === 304) {
|
||||||
|
return Promise.resolve(etags.getCachedPayload(url));
|
||||||
|
} else {
|
||||||
|
const payloadPromise = response ?
|
||||||
|
this.getResponseObject(response) : Promise.resolve();
|
||||||
|
payloadPromise.then(payload => {
|
||||||
|
etags.collect(url, response, payload);
|
||||||
|
});
|
||||||
|
return payloadPromise;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getChangeCommitInfo(changeNum, patchNum) {
|
getChangeCommitInfo(changeNum, patchNum) {
|
||||||
|
@@ -441,10 +441,12 @@ limitations under the License.
|
|||||||
const authErrorStub = sandbox.stub();
|
const authErrorStub = sandbox.stub();
|
||||||
element.addEventListener('auth-error', authErrorStub);
|
element.addEventListener('auth-error', authErrorStub);
|
||||||
element.fetchJSON('/bar').then(r => {
|
element.fetchJSON('/bar').then(r => {
|
||||||
assert.isTrue(authErrorStub.called);
|
flush(() => {
|
||||||
assert.isFalse(serverErrorStub.called);
|
assert.isTrue(authErrorStub.called);
|
||||||
assert.isNull(element._cache['/accounts/self/detail']);
|
assert.isFalse(serverErrorStub.called);
|
||||||
done();
|
assert.isNull(element._cache['/accounts/self/detail']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user