mirror of
https://github.com/status-im/consul.git
synced 2025-01-22 19:50:36 +00:00
ui: Adds XHR connection management to HTTP/1.1 installs (#5083)
Adds xhr connection managment to http/1.1 installs This includes various things: 1. An object pool to 'acquire', 'release' and 'dispose' of objects, also a 'purge' to completely empty it 2. A `Request` data object, mainly for reasoning about the object better 3. A pseudo http 'client' which doens't actually control the request itself but does help to manage the connections An initializer is used to detect the script element of the consul-ui sourcecode which we use later to sniff the protocol that we are most likely using for API access
This commit is contained in:
parent
4d183ef743
commit
516610eb0b
@ -1,10 +1,12 @@
|
|||||||
import Adapter from 'ember-data/adapters/rest';
|
import Adapter from 'ember-data/adapters/rest';
|
||||||
import { AbortError } from 'ember-data/adapters/errors';
|
import { AbortError } from 'ember-data/adapters/errors';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import { get } from '@ember/object';
|
||||||
|
|
||||||
import URL from 'url';
|
import URL from 'url';
|
||||||
import createURL from 'consul-ui/utils/createURL';
|
import createURL from 'consul-ui/utils/createURL';
|
||||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||||
|
import { HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL } from 'consul-ui/utils/http/consul';
|
||||||
|
|
||||||
export const REQUEST_CREATE = 'createRecord';
|
export const REQUEST_CREATE = 'createRecord';
|
||||||
export const REQUEST_READ = 'queryRecord';
|
export const REQUEST_READ = 'queryRecord';
|
||||||
@ -14,10 +16,31 @@ export const REQUEST_DELETE = 'deleteRecord';
|
|||||||
|
|
||||||
export const DATACENTER_QUERY_PARAM = 'dc';
|
export const DATACENTER_QUERY_PARAM = 'dc';
|
||||||
|
|
||||||
import { HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL } from 'consul-ui/utils/http/consul';
|
|
||||||
export default Adapter.extend({
|
export default Adapter.extend({
|
||||||
namespace: 'v1',
|
namespace: 'v1',
|
||||||
repo: service('settings'),
|
repo: service('settings'),
|
||||||
|
client: service('client/http'),
|
||||||
|
manageConnection: function(options) {
|
||||||
|
const client = get(this, 'client');
|
||||||
|
const complete = options.complete;
|
||||||
|
const beforeSend = options.beforeSend;
|
||||||
|
options.beforeSend = function(xhr) {
|
||||||
|
if (typeof beforeSend === 'function') {
|
||||||
|
beforeSend(...arguments);
|
||||||
|
}
|
||||||
|
options.id = client.request(options, xhr);
|
||||||
|
};
|
||||||
|
options.complete = function(xhr, textStatus) {
|
||||||
|
client.complete(options.id);
|
||||||
|
if (typeof complete === 'function') {
|
||||||
|
complete(...arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
_ajaxRequest: function(options) {
|
||||||
|
return this._super(this.manageConnection(options));
|
||||||
|
},
|
||||||
queryRecord: function() {
|
queryRecord: function() {
|
||||||
return this._super(...arguments).catch(function(e) {
|
return this._super(...arguments).catch(function(e) {
|
||||||
if (e instanceof AbortError) {
|
if (e instanceof AbortError) {
|
||||||
|
15
ui-v2/app/initializers/client.js
Normal file
15
ui-v2/app/initializers/client.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const scripts = document.getElementsByTagName('script');
|
||||||
|
const current = scripts[scripts.length - 1];
|
||||||
|
|
||||||
|
export function initialize(application) {
|
||||||
|
const Client = application.resolveRegistration('service:client/http');
|
||||||
|
Client.reopen({
|
||||||
|
isCurrent: function(src) {
|
||||||
|
return current.src === src;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initialize,
|
||||||
|
};
|
87
ui-v2/app/services/client/http.js
Normal file
87
ui-v2/app/services/client/http.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
|
import { get, set } from '@ember/object';
|
||||||
|
import { Promise } from 'rsvp';
|
||||||
|
|
||||||
|
import getObjectPool from 'consul-ui/utils/get-object-pool';
|
||||||
|
import Request from 'consul-ui/utils/http/request';
|
||||||
|
|
||||||
|
const dispose = function(request) {
|
||||||
|
if (request.headers()['content-type'] === 'text/event-stream') {
|
||||||
|
const xhr = request.connection();
|
||||||
|
// unsent and opened get aborted
|
||||||
|
// headers and loading means wait for it
|
||||||
|
// to finish for the moment
|
||||||
|
if (xhr.readyState) {
|
||||||
|
switch (xhr.readyState) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
xhr.abort();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
export default Service.extend({
|
||||||
|
dom: service('dom'),
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
let protocol = 'http/1.1';
|
||||||
|
try {
|
||||||
|
protocol = performance.getEntriesByType('resource').find(item => {
|
||||||
|
// isCurrent is added in initializers/client and is used
|
||||||
|
// to ensure we use the consul-ui.js src to sniff what the protocol
|
||||||
|
// is. Based on the assumption that whereever this script is it's
|
||||||
|
// likely to be the same as the xmlhttprequests
|
||||||
|
return item.initiatorType === 'script' && this.isCurrent(item.name);
|
||||||
|
}).nextHopProtocol;
|
||||||
|
} catch (e) {
|
||||||
|
// pass through
|
||||||
|
}
|
||||||
|
let maxConnections;
|
||||||
|
// http/2, http2+QUIC/39 and SPDY don't have connection limits
|
||||||
|
switch (true) {
|
||||||
|
case protocol.indexOf('h2') === 0:
|
||||||
|
case protocol.indexOf('hq') === 0:
|
||||||
|
case protocol.indexOf('spdy') === 0:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// generally 6 are available
|
||||||
|
// reserve 1 for traffic that we can't manage
|
||||||
|
maxConnections = 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
set(this, 'connections', getObjectPool(dispose, maxConnections));
|
||||||
|
if (typeof maxConnections !== 'undefined') {
|
||||||
|
set(this, 'maxConnections', maxConnections);
|
||||||
|
const doc = get(this, 'dom').document();
|
||||||
|
// when the user hides the tab, abort all connections
|
||||||
|
doc.addEventListener('visibilitychange', e => {
|
||||||
|
if (e.target.hidden) {
|
||||||
|
get(this, 'connections').purge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
whenAvailable: function(e) {
|
||||||
|
const doc = get(this, 'dom').document();
|
||||||
|
// if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch)
|
||||||
|
// any aborted errors should restart
|
||||||
|
if (typeof get(this, 'maxConnections') !== 'undefined' && doc.hidden) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
doc.addEventListener('visibilitychange', function listen(event) {
|
||||||
|
doc.removeEventListener('visibilitychange', listen);
|
||||||
|
resolve(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(e);
|
||||||
|
},
|
||||||
|
request: function(options, xhr) {
|
||||||
|
const request = new Request(options.type, options.url, { body: options.data || {} }, xhr);
|
||||||
|
return get(this, 'connections').acquire(request, request.getId());
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
return get(this, 'connections').release(...arguments);
|
||||||
|
},
|
||||||
|
});
|
@ -50,7 +50,7 @@ export default Service.extend({
|
|||||||
},
|
},
|
||||||
elementsByTagName: function(name, context) {
|
elementsByTagName: function(name, context) {
|
||||||
context = typeof context === 'undefined' ? get(this, 'doc') : context;
|
context = typeof context === 'undefined' ? get(this, 'doc') : context;
|
||||||
return context.getElementByTagName(name);
|
return context.getElementsByTagName(name);
|
||||||
},
|
},
|
||||||
elements: function(selector, context) {
|
elements: function(selector, context) {
|
||||||
// don't ever be tempted to [...$$()] here
|
// don't ever be tempted to [...$$()] here
|
||||||
|
52
ui-v2/app/utils/get-object-pool.js
Normal file
52
ui-v2/app/utils/get-object-pool.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export default function(dispose = function() {}, max, objects = []) {
|
||||||
|
return {
|
||||||
|
acquire: function(obj, id) {
|
||||||
|
// TODO: what should happen if an ID already exists
|
||||||
|
// should we ignore and release both? Or prevent from acquiring? Or generate a unique ID?
|
||||||
|
// what happens if we can't get an id via getId or .id?
|
||||||
|
// could potentially use Set
|
||||||
|
objects.push(obj);
|
||||||
|
if (typeof max !== 'undefined') {
|
||||||
|
if (objects.length > max) {
|
||||||
|
return dispose(objects.shift());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
// release releases the obj from the pool but **doesn't** dispose it
|
||||||
|
release: function(obj) {
|
||||||
|
let index = -1;
|
||||||
|
let id;
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
id = obj;
|
||||||
|
} else {
|
||||||
|
id = obj.id;
|
||||||
|
}
|
||||||
|
objects.forEach(function(item, i) {
|
||||||
|
let itemId;
|
||||||
|
if (typeof item.getId === 'function') {
|
||||||
|
itemId = item.getId();
|
||||||
|
} else {
|
||||||
|
itemId = item.id;
|
||||||
|
}
|
||||||
|
if (itemId === id) {
|
||||||
|
index = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (index !== -1) {
|
||||||
|
return objects.splice(index, 1)[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
purge: function() {
|
||||||
|
let obj;
|
||||||
|
const objs = [];
|
||||||
|
while ((obj = objects.shift())) {
|
||||||
|
objs.push(dispose(obj));
|
||||||
|
}
|
||||||
|
return objs;
|
||||||
|
},
|
||||||
|
dispose: function(id) {
|
||||||
|
return dispose(this.release(id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
29
ui-v2/app/utils/http/request.js
Normal file
29
ui-v2/app/utils/http/request.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export default class {
|
||||||
|
constructor(method, url, headers, xhr) {
|
||||||
|
this._xhr = xhr;
|
||||||
|
this._url = url;
|
||||||
|
this._method = method;
|
||||||
|
this._headers = headers;
|
||||||
|
this._headers = {
|
||||||
|
...headers,
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-request-id': `${this._method} ${this._url}?${JSON.stringify(headers.body)}`,
|
||||||
|
};
|
||||||
|
if (typeof this._headers.body.index !== 'undefined') {
|
||||||
|
// this should probably be in a response
|
||||||
|
this._headers['content-type'] = 'text/event-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers() {
|
||||||
|
return this._headers;
|
||||||
|
}
|
||||||
|
getId() {
|
||||||
|
return this._headers['x-request-id'];
|
||||||
|
}
|
||||||
|
abort() {
|
||||||
|
this._xhr.abort();
|
||||||
|
}
|
||||||
|
connection() {
|
||||||
|
return this._xhr;
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
|
|||||||
import repo from 'consul-ui/tests/helpers/repo';
|
import repo from 'consul-ui/tests/helpers/repo';
|
||||||
const NAME = 'intention';
|
const NAME = 'intention';
|
||||||
moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
|
moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
|
||||||
// Specify the other units that are required for this test.
|
integration: true,
|
||||||
needs: [
|
|
||||||
'service:settings',
|
|
||||||
'service:store',
|
|
||||||
`adapter:${NAME}`,
|
|
||||||
`serializer:${NAME}`,
|
|
||||||
`model:${NAME}`,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dc = 'dc-1';
|
const dc = 'dc-1';
|
||||||
|
12
ui-v2/tests/unit/services/client/http-test.js
Normal file
12
ui-v2/tests/unit/services/client/http-test.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { moduleFor, test } from 'ember-qunit';
|
||||||
|
|
||||||
|
moduleFor('service:client/http', 'Unit | Service | client/http', {
|
||||||
|
// Specify the other units that are required for this test.
|
||||||
|
needs: ['service:dom'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace this with your real tests.
|
||||||
|
test('it exists', function(assert) {
|
||||||
|
let service = this.subject();
|
||||||
|
assert.ok(service);
|
||||||
|
});
|
98
ui-v2/tests/unit/utils/get-object-pool-test.js
Normal file
98
ui-v2/tests/unit/utils/get-object-pool-test.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import getObjectPool from 'consul-ui/utils/get-object-pool';
|
||||||
|
import { module, skip } from 'qunit';
|
||||||
|
import test from 'ember-sinon-qunit/test-support/test';
|
||||||
|
|
||||||
|
module('Unit | Utility | get object pool');
|
||||||
|
|
||||||
|
skip('Decide what to do if you add 2 objects with the same id');
|
||||||
|
test('acquire adds objects', function(assert) {
|
||||||
|
const actual = [];
|
||||||
|
const expected = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-123',
|
||||||
|
};
|
||||||
|
const expected2 = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-456',
|
||||||
|
};
|
||||||
|
const pool = getObjectPool(function() {}, 10, actual);
|
||||||
|
pool.acquire(expected, expected.id);
|
||||||
|
assert.deepEqual(actual[0], expected);
|
||||||
|
pool.acquire(expected2, expected2.id);
|
||||||
|
assert.deepEqual(actual[1], expected2);
|
||||||
|
});
|
||||||
|
test('acquire adds objects and returns the id', function(assert) {
|
||||||
|
const arr = [];
|
||||||
|
const expected = 'hi-there-123';
|
||||||
|
const obj = {
|
||||||
|
hi: 'there',
|
||||||
|
id: expected,
|
||||||
|
};
|
||||||
|
const pool = getObjectPool(function() {}, 10, arr);
|
||||||
|
const actual = pool.acquire(obj, expected);
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
test('acquire adds objects, and disposes when there is no room', function(assert) {
|
||||||
|
const actual = [];
|
||||||
|
const expected = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-123',
|
||||||
|
};
|
||||||
|
const expected2 = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-456',
|
||||||
|
};
|
||||||
|
const dispose = this.stub()
|
||||||
|
.withArgs(expected)
|
||||||
|
.returnsArg(0);
|
||||||
|
const pool = getObjectPool(dispose, 1, actual);
|
||||||
|
pool.acquire(expected, expected.id);
|
||||||
|
assert.deepEqual(actual[0], expected);
|
||||||
|
pool.acquire(expected2, expected2.id);
|
||||||
|
assert.deepEqual(actual[0], expected2);
|
||||||
|
assert.ok(dispose.calledOnce);
|
||||||
|
});
|
||||||
|
test('it disposes', function(assert) {
|
||||||
|
const arr = [];
|
||||||
|
const expected = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-123',
|
||||||
|
};
|
||||||
|
const expected2 = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-456',
|
||||||
|
};
|
||||||
|
const dispose = this.stub().returnsArg(0);
|
||||||
|
const pool = getObjectPool(dispose, 2, arr);
|
||||||
|
const id = pool.acquire(expected, expected.id);
|
||||||
|
assert.deepEqual(arr[0], expected);
|
||||||
|
pool.acquire(expected2, expected2.id);
|
||||||
|
assert.deepEqual(arr[1], expected2);
|
||||||
|
const actual = pool.dispose(id);
|
||||||
|
assert.ok(dispose.calledOnce);
|
||||||
|
assert.equal(arr.length, 1, 'object was removed from array');
|
||||||
|
assert.deepEqual(actual, expected, 'returned object is expected object');
|
||||||
|
assert.deepEqual(arr[0], expected2, 'object in the pool is expected object');
|
||||||
|
});
|
||||||
|
test('it purges', function(assert) {
|
||||||
|
const arr = [];
|
||||||
|
const expected = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-123',
|
||||||
|
};
|
||||||
|
const expected2 = {
|
||||||
|
hi: 'there',
|
||||||
|
id: 'hi-there-456',
|
||||||
|
};
|
||||||
|
const dispose = this.stub().returnsArg(0);
|
||||||
|
const pool = getObjectPool(dispose, 2, arr);
|
||||||
|
pool.acquire(expected, expected.id);
|
||||||
|
assert.deepEqual(arr[0], expected);
|
||||||
|
pool.acquire(expected2, expected2.id);
|
||||||
|
assert.deepEqual(arr[1], expected2);
|
||||||
|
const actual = pool.purge();
|
||||||
|
assert.ok(dispose.calledTwice, 'dispose was called on everything');
|
||||||
|
assert.equal(arr.length, 0, 'the pool is empty');
|
||||||
|
assert.deepEqual(actual[0], expected, 'the first purged object is correct');
|
||||||
|
assert.deepEqual(actual[1], expected2, 'the second purged object is correct');
|
||||||
|
});
|
10
ui-v2/tests/unit/utils/http/request-test.js
Normal file
10
ui-v2/tests/unit/utils/http/request-test.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import httpRequest from 'consul-ui/utils/http/request';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
|
module('Unit | Utility | http/request');
|
||||||
|
|
||||||
|
// Replace this with your real tests.
|
||||||
|
test('it works', function(assert) {
|
||||||
|
const actual = httpRequest;
|
||||||
|
assert.ok(typeof actual === 'function');
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user