276 lines
9.8 KiB
JavaScript
276 lines
9.8 KiB
JavaScript
/*global $*/
|
|
import Service, { inject as service } from '@ember/service';
|
|
import { get, set } from '@ember/object';
|
|
|
|
import { CACHE_CONTROL, CONTENT_TYPE } from 'consul-ui/utils/http/headers';
|
|
|
|
import { HEADERS_TOKEN as CONSUL_TOKEN } from 'consul-ui/utils/http/consul';
|
|
|
|
import { env } from 'consul-ui/env';
|
|
import getObjectPool from 'consul-ui/utils/get-object-pool';
|
|
import Request from 'consul-ui/utils/http/request';
|
|
import createURL from 'consul-ui/utils/createURL';
|
|
|
|
// reopen EventSources if a user changes tab
|
|
export const restartWhenAvailable = function(client) {
|
|
return function(e) {
|
|
// setup the aborted connection restarting
|
|
// this should happen here to avoid cache deletion
|
|
const status = get(e, 'errors.firstObject.status');
|
|
if (status === '0') {
|
|
// Any '0' errors (abort) should possibly try again, depending upon the circumstances
|
|
// whenAvailable returns a Promise that resolves when the client is available
|
|
// again
|
|
return client.whenAvailable(e);
|
|
}
|
|
throw e;
|
|
};
|
|
};
|
|
class HTTPError extends Error {
|
|
constructor(statusCode, message) {
|
|
super(message);
|
|
this.statusCode = statusCode;
|
|
}
|
|
}
|
|
const dispose = function(request) {
|
|
if (request.headers()[CONTENT_TYPE.toLowerCase()] === '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;
|
|
};
|
|
// TODO: Potentially url should check if any of the params
|
|
// passed to it are undefined (null is fine). We could then get rid of the
|
|
// multitude of checks we do throughout the adapters
|
|
// right now createURL converts undefined to '' so we need to check thats not needed
|
|
// anywhere (todo written here for visibility)
|
|
const url = createURL(encodeURIComponent);
|
|
const createHeaders = function(lines) {
|
|
return lines.reduce(function(prev, item) {
|
|
const temp = item.split(':');
|
|
if (temp.length > 1) {
|
|
prev[temp[0].trim()] = temp[1].trim();
|
|
}
|
|
return prev;
|
|
}, {});
|
|
};
|
|
export default Service.extend({
|
|
dom: service('dom'),
|
|
settings: service('settings'),
|
|
init: function() {
|
|
this._super(...arguments);
|
|
this._listeners = this.dom.listeners();
|
|
const maxConnections = env('CONSUL_HTTP_MAX_CONNECTIONS');
|
|
set(this, 'connections', getObjectPool(dispose, maxConnections));
|
|
if (typeof maxConnections !== 'undefined') {
|
|
set(this, 'maxConnections', maxConnections);
|
|
// when the user hides the tab, abort all connections
|
|
this._listeners.add(this.dom.document(), {
|
|
visibilitychange: e => {
|
|
if (e.target.hidden) {
|
|
this.connections.purge();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
},
|
|
willDestroy: function() {
|
|
this._listeners.remove();
|
|
this.connections.purge();
|
|
set(this, 'connections', undefined);
|
|
this._super(...arguments);
|
|
},
|
|
url: function() {
|
|
return url(...arguments);
|
|
},
|
|
body: function(strs, ...values) {
|
|
let body = {};
|
|
const doubleBreak = strs.reduce(function(prev, item, i) {
|
|
// Ensure each line has no whitespace either end, including empty lines
|
|
item = item
|
|
.split('\n')
|
|
.map(item => item.trim())
|
|
.join('\n');
|
|
if (item.indexOf('\n\n') !== -1) {
|
|
return i;
|
|
}
|
|
return prev;
|
|
}, -1);
|
|
if (doubleBreak !== -1) {
|
|
// This merges request bodies together, so you can specify multiple bodies
|
|
// in the request and it will merge them together.
|
|
// Turns out we never actually do this, so it might be worth removing as it complicates
|
|
// matters slightly as we assumed post bodies would be an object.
|
|
// This actually works as it just uses the value of the first object, if its an array
|
|
// it concats
|
|
body = values.splice(doubleBreak).reduce(function(prev, item, i) {
|
|
switch (true) {
|
|
case Array.isArray(item):
|
|
if (i === 0) {
|
|
prev = [];
|
|
}
|
|
return prev.concat(item);
|
|
case typeof item !== 'string':
|
|
return {
|
|
...prev,
|
|
...item,
|
|
};
|
|
default:
|
|
return item;
|
|
}
|
|
}, body);
|
|
}
|
|
return [body, ...values];
|
|
},
|
|
request: function(cb) {
|
|
const client = this;
|
|
return cb(function(strs, ...values) {
|
|
// first go to the end and remove/parse the http body
|
|
const [body, ...urlVars] = client.body(...arguments);
|
|
// with whats left get the method off the front
|
|
const [method, ...urlParts] = client.url(strs, ...urlVars).split(' ');
|
|
// with whats left use the rest of the line for the url
|
|
// with whats left after the line, use for the headers
|
|
const [url, ...headerParts] = urlParts.join(' ').split('\n');
|
|
|
|
return client.settings.findBySlug('token').then(function(token) {
|
|
const requestHeaders = createHeaders(headerParts);
|
|
const headers = {
|
|
// default to application/json
|
|
...{
|
|
[CONTENT_TYPE]: 'application/json; charset=utf-8',
|
|
},
|
|
// add any application level headers
|
|
...{
|
|
[CONSUL_TOKEN]: typeof token.SecretID === 'undefined' ? '' : token.SecretID,
|
|
},
|
|
// but overwrite or add to those from anything in the specific request
|
|
...requestHeaders,
|
|
};
|
|
// We use cache-control in the response
|
|
// but we don't want to send it, but we artificially
|
|
// tag it onto the response below if it is set on the request
|
|
delete headers[CACHE_CONTROL];
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
const options = {
|
|
url: url.trim(),
|
|
method: method,
|
|
contentType: headers[CONTENT_TYPE],
|
|
// type: 'json',
|
|
complete: function(xhr, textStatus) {
|
|
client.complete(this.id);
|
|
},
|
|
success: function(response, status, xhr) {
|
|
const headers = createHeaders(xhr.getAllResponseHeaders().split('\n'));
|
|
if (typeof requestHeaders[CACHE_CONTROL] !== 'undefined') {
|
|
// if cache-control was on the request, artificially tag
|
|
// it back onto the response, also see comment above
|
|
headers[CACHE_CONTROL] = requestHeaders[CACHE_CONTROL];
|
|
}
|
|
const respond = function(cb) {
|
|
return cb(headers, response);
|
|
};
|
|
// TODO: nextTick ?
|
|
resolve(respond);
|
|
},
|
|
error: function(xhr, textStatus, err) {
|
|
let error;
|
|
if (err instanceof Error) {
|
|
error = err;
|
|
} else {
|
|
let status = xhr.status;
|
|
// TODO: Not sure if we actually need this, but ember-data checks it
|
|
if (textStatus === 'abort') {
|
|
status = 0;
|
|
}
|
|
if (textStatus === 'timeout') {
|
|
status = 408;
|
|
}
|
|
error = new HTTPError(status, xhr.responseText);
|
|
}
|
|
//TODO: nextTick ?
|
|
reject(error);
|
|
},
|
|
converters: {
|
|
'text json': function(response) {
|
|
try {
|
|
return $.parseJSON(response);
|
|
} catch (e) {
|
|
return response;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
if (typeof body !== 'undefined') {
|
|
// Only read add HTTP body if we aren't GET
|
|
// Right now we do this to avoid having to put data in the templates
|
|
// for write-like actions
|
|
// potentially we should change things so you _have_ to do that
|
|
// as doing it this way is a little magical
|
|
if (method !== 'GET' && headers[CONTENT_TYPE].indexOf('json') !== -1) {
|
|
options.data = JSON.stringify(body);
|
|
} else {
|
|
// TODO: Does this need urlencoding? Assuming jQuery does this
|
|
options.data = body;
|
|
}
|
|
}
|
|
// temporarily reset the headers/content-type so it works the same
|
|
// as previously, should be able to remove this once the data layer
|
|
// rewrite is over and we can assert sending via form-encoded is fine
|
|
// also see adapters/kv content-types in requestForCreate/UpdateRecord
|
|
// also see https://github.com/hashicorp/consul/issues/3804
|
|
options.contentType = 'application/json; charset=utf-8';
|
|
headers[CONTENT_TYPE] = options.contentType;
|
|
//
|
|
options.beforeSend = function(xhr) {
|
|
if (headers) {
|
|
Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
|
|
}
|
|
this.id = client.acquire(options, xhr);
|
|
};
|
|
return $.ajax(options);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
abort: function(id = null) {
|
|
this.connections.purge();
|
|
},
|
|
whenAvailable: function(e) {
|
|
// if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch)
|
|
// any aborted errors should restart
|
|
const doc = this.dom.document();
|
|
if (typeof this.maxConnections !== 'undefined' && doc.hidden) {
|
|
return new Promise(resolve => {
|
|
const remove = this._listeners.add(doc, {
|
|
visibilitychange: function(event) {
|
|
remove();
|
|
// we resolve with the event that comes from
|
|
// whenAvailable not visibilitychange
|
|
resolve(e);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
return Promise.resolve(e);
|
|
},
|
|
acquire: function(options, xhr) {
|
|
const request = new Request(options.method, options.url, { body: options.data || {} }, xhr);
|
|
return this.connections.acquire(request, request.getId());
|
|
},
|
|
complete: function() {
|
|
return this.connections.release(...arguments);
|
|
},
|
|
});
|