-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
329 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); |