UI: Add EventSource ready for implementing blocking queries (#5070)

- Maintain http headers as JSON-API meta for all API requests (#4946)
- Add EventSource ready for implementing blocking queries
- EventSource project implementation to enable blocking queries for service and node listings (#5267)
- Add setting to enable/disable blocking queries (#5352)
This commit is contained in:
John Cowen 2019-02-21 10:36:15 +00:00 committed by John Cowen
parent f225da36f1
commit cb0c5309c9
45 changed files with 1878 additions and 56 deletions

1
ui-v2/.eslintignore Normal file
View File

@ -0,0 +1 @@
app/utils/dom/event-target/event-target-shim/event.js

View File

@ -114,6 +114,9 @@ export default Adapter.extend({
if (typeof query.separator !== 'undefined') {
delete query.separator;
}
if (typeof query.index !== 'undefined') {
delete query.index;
}
delete _query[DATACENTER_QUERY_PARAM];
return query;
},

View File

@ -1,9 +1,10 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import { get } from '@ember/object';
export default Controller.extend(WithSearching, WithHealthFiltering, {
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
init: function() {
this.searchParams = {
healthyNode: 's',

View File

@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
const max = function(arr, prop) {
@ -25,7 +26,7 @@ const width = function(num) {
const widthDeclaration = function(num) {
return htmlSafe(`width: ${num}px`);
};
export default Controller.extend(WithSearching, WithHealthFiltering, {
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
init: function() {
this.searchParams = {
service: 's',
@ -52,14 +53,14 @@ export default Controller.extend(WithSearching, WithHealthFiltering, {
remainingWidth: computed('maxWidth', function() {
return htmlSafe(`width: calc(50% - ${Math.round(get(this, 'maxWidth') / 2)}px)`);
}),
maxPassing: computed('items', function() {
return max(get(this, 'items'), 'ChecksPassing');
maxPassing: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksPassing');
}),
maxWarning: computed('items', function() {
return max(get(this, 'items'), 'ChecksWarning');
maxWarning: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksWarning');
}),
maxCritical: computed('items', function() {
return max(get(this, 'items'), 'ChecksCritical');
maxCritical: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksCritical');
}),
passingWidth: computed('maxPassing', function() {
return widthDeclaration(width(get(this, 'maxPassing')));

View File

@ -0,0 +1,29 @@
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
export default Controller.extend({
repo: service('settings'),
dom: service('dom'),
actions: {
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
// TODO: Switch to using forms like the rest of the app
// setting utils/form/builder for things to be done before we
// can do that. For the moment just do things normally its a simple
// enough form at the moment
const target = event.target;
const blocking = get(this, 'item.client.blocking');
switch (target.name) {
case 'client[blocking]':
if (typeof blocking === 'undefined') {
set(this, 'item.client', {});
}
set(this, 'item.client.blocking', !blocking);
this.send('update', get(this, 'item'));
break;
}
},
},
});

View File

@ -0,0 +1,61 @@
import config from '../config/environment';
const enabled = 'CONSUL_UI_DISABLE_REALTIME';
export function initialize(container) {
if (config[enabled] || window.localStorage.getItem(enabled) !== null) {
return;
}
['node', 'service']
.map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
service: `repository/${item}/event-source`,
extend: 'repository/type/event-source',
// Inject our original respository that is used by this class
// within the callable of the EventSource
services: {
content: `repository/${item}`,
},
};
})
.concat([
// These are the routes where we overwrite the 'default'
// repo service. Default repos are repos that return a promise resovlving to
// an ember-data record or recordset
{
route: 'dc/nodes/index',
services: {
repo: 'repository/node/event-source',
},
},
{
route: 'dc/services/index',
services: {
repo: 'repository/service/event-source',
},
},
])
.forEach(function(definition) {
if (typeof definition.extend !== 'undefined') {
// Create the class instances that we need
container.register(
`service:${definition.service}`,
container.resolveRegistration(`service:${definition.extend}`).extend({})
);
}
Object.keys(definition.services).forEach(function(name) {
const servicePath = definition.services[name];
// inject its dependencies, this could probably detect the type
// but hardcode this for the moment
if (typeof definition.route !== 'undefined') {
container.inject(`route:${definition.route}`, name, `service:${servicePath}`);
} else {
container.inject(`service:${definition.service}`, name, `service:${servicePath}`);
}
});
});
}
export default {
initialize,
};

View File

@ -0,0 +1,16 @@
import Mixin from '@ember/object/mixin';
export default Mixin.create({
reset: function(exiting) {
if (exiting) {
Object.keys(this).forEach(prop => {
if (this[prop] && typeof this[prop].close === 'function') {
this[prop].close();
// ember doesn't delete on 'resetController' by default
delete this[prop];
}
});
}
return this._super(...arguments);
},
});

View File

@ -15,7 +15,7 @@ const toKeyValue = function(el) {
};
export default Mixin.create({
filters: {},
filtered: computed('items', 'filters', function() {
filtered: computed('items.[]', 'filters', function() {
const filters = get(this, 'filters');
return get(this, 'items').filter(item => {
return this.filter(item, filters);

View File

@ -29,7 +29,7 @@ export default Mixin.create(WithFiltering, {
as: 'filter',
},
},
healthFilters: computed('items', function() {
healthFilters: computed('items.[]', function() {
const items = get(this, 'items');
const objs = ['', 'passing', 'warning', 'critical'].map(function(item) {
const count = countStatus(items, item);

View File

@ -88,9 +88,9 @@ export const routes = {
_options: { path: '/' },
},
// The settings page is global.
// settings: {
// _options: { path: '/setting' },
// },
settings: {
_options: { path: '/setting' },
},
notfound: {
_options: { path: '/*path' },
},

View File

@ -3,8 +3,8 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Route.extend(WithBlockingActions, {
export default Route.extend({
client: service('client/http'),
repo: service('settings'),
dcRepo: service('repository/dc'),
model: function(params) {
@ -24,8 +24,12 @@ export default Route.extend(WithBlockingActions, {
this._super(...arguments);
controller.setProperties(model);
},
// overwrite afterUpdate and afterDelete hooks
// to avoid the default 'return to listing page'
afterUpdate: function() {},
afterDelete: function() {},
actions: {
update: function(item) {
if (!get(item, 'client.blocking')) {
get(this, 'client').abort();
}
get(this, 'repo').persist(item);
},
},
});

View File

@ -63,6 +63,9 @@ export default Service.extend({
});
}
},
abort: function(id = null) {
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)

View File

@ -0,0 +1,27 @@
import Service from '@ember/service';
import { get } from '@ember/object';
export default Service.extend({
shouldProxy: function(content, method) {
return false;
},
init: function() {
this._super(...arguments);
const content = get(this, 'content');
for (let prop in content) {
if (typeof content[prop] === 'function') {
if (this.shouldProxy(content, prop)) {
this[prop] = function() {
return this.execute(content, prop).then(method => {
return method.apply(this, arguments);
});
};
} else if (typeof this[prop] !== 'function') {
this[prop] = function() {
return content[prop](...arguments);
};
}
}
}
},
});

View File

@ -0,0 +1,92 @@
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import LazyProxyService from 'consul-ui/services/lazy-proxy';
import { cache as createCache, BlockingEventSource } from 'consul-ui/utils/dom/event-source';
const createProxy = function(repo, find, settings, cache, serialize = JSON.stringify) {
// proxied find*..(id, dc)
const throttle = get(this, 'wait').execute;
const client = get(this, 'client');
return function() {
const key = `${repo.getModelName()}.${find}.${serialize([...arguments])}`;
const _args = arguments;
const newPromisedEventSource = cache;
return newPromisedEventSource(
function(configuration) {
// take a copy of the original arguments
// this means we don't have any configuration object on it
let args = [..._args];
if (configuration.settings.enabled) {
// ...and only add our current cursor/configuration if we are blocking
args = args.concat([configuration]);
}
// save a callback so we can conditionally throttle
const cb = () => {
// original find... with configuration now added
return repo[find](...args)
.then(res => {
if (!configuration.settings.enabled) {
// blocking isn't enabled, immediately close
this.close();
}
return res;
})
.catch(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;
});
};
// if we have a cursor (which means its at least the second call)
// and we have a throttle setting, wait for so many ms
if (typeof configuration.cursor !== 'undefined' && configuration.settings.throttle) {
return throttle(configuration.settings.throttle).then(cb);
}
return cb();
},
{
key: key,
type: BlockingEventSource,
settings: {
enabled: settings.blocking,
throttle: settings.throttle,
},
}
);
};
};
let cache = null;
export default LazyProxyService.extend({
store: service('store'),
settings: service('settings'),
wait: service('timeout'),
client: service('client/http'),
init: function() {
this._super(...arguments);
if (cache === null) {
cache = createCache({});
}
},
willDestroy: function() {
cache = null;
},
shouldProxy: function(content, method) {
return method.indexOf('find') === 0;
},
execute: function(repo, find) {
return get(this, 'settings')
.findBySlug('client')
.then(settings => {
return createProxy.bind(this)(repo, find, settings, cache);
});
},
});

View File

@ -44,11 +44,9 @@
<li data-test-main-nav-docs>
<a href="{{ env 'CONSUL_DOCUMENTATION_URL'}}/index.html" rel="help noopener noreferrer" target="_blank">Documentation</a>
</li>
{{#if false }}
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
<a href={{href-to 'settings'}}>Settings</a>
</li>
{{/if}}
</ul>
</nav>
</div>

View File

@ -1,20 +1,5 @@
{{#hashicorp-consul id="wrapper" dcs=dcs dc=dc}}
{{#app-view class="settings show"}}
{{#block-slot 'notification' as |status type|}}
{{#if (eq type 'update')}}
{{#if (eq status 'success') }}
Your settings were saved.
{{else}}
There was an error saving your settings.
{{/if}}
{{ else if (eq type 'delete')}}
{{#if (eq status 'success') }}
You settings have been reset.
{{else}}
There was an error resetting your settings.
{{/if}}
{{/if}}
{{/block-slot}}
{{#block-slot 'header'}}
<h1>
Settings
@ -26,13 +11,13 @@
</p>
<form>
<fieldset>
<label class="type-text">
<span>ACL Token</span>
{{ input type='password' value=item.token name="token" }}
<em>The token is sent with requests as the <code>X-Consul-Token</code> HTTP header parameter. This is used to control the ACL for the web UI.</em>
</label>
<div class="type-toggle">
<label>
<input type="checkbox" name="client[blocking]" checked={{if item.client.blocking 'checked' }} onchange={{action 'change'}} />
<span>Enable Catalog realtime updates (blocking queries)</span>
</label>
</div>
</fieldset>
<button type="submit" {{action 'update' item}}>Save</button>
</form>
{{/block-slot}}
{{/app-view}}

View File

@ -0,0 +1,92 @@
import { get } from '@ember/object';
import { Promise } from 'rsvp';
// native EventSource retry is ~3s wait
export const create5xxBackoff = function(ms = 3000, P = Promise, wait = setTimeout) {
// This expects an ember-data like error
return function(err) {
const status = get(err, 'errors.firstObject.status');
if (typeof status !== 'undefined') {
switch (true) {
// Any '5xx' (not 500) errors should back off and try again
case status.indexOf('5') === 0 && status.length === 3 && status !== '500':
return new P(function(resolve) {
wait(function() {
resolve(err);
}, ms);
});
}
}
// any other errors should throw to be picked up by an error listener/catch
throw err;
};
};
const defaultCreateEvent = function(result, configuration) {
return {
type: 'message',
data: result,
};
};
/**
* Wraps an EventSource with functionality to add native EventSource-like functionality
*
* @param {Class} [CallableEventSource] - CallableEventSource Class
* @param {Function} [backoff] - Default backoff function for all instances, defaults to create5xxBackoff
*/
export default function(EventSource, backoff = create5xxBackoff()) {
/**
* An EventSource implementation to add native EventSource-like functionality with just callbacks (`cursor` and 5xx backoff)
*
* This includes:
* 1. 5xx backoff support (uses a 3 second reconnect like native implementations). You can add to this via `Promise.catch`
* 2. A `cursor` configuration value. Current `cursor` is taken from the `meta` property of the event (i.e. `event.data.meta.cursor`)
* 3. Event data can be customized by adding a `configuration.createEvent`
*
* @param {Function} [source] - Promise returning function that resolves your data
* @param {Object} [configuration] - Plain configuration object:
* `cursor` - Cursor position of the EventSource
* `createEvent` - A data filter, giving you the opportunity to filter or replace the event data, such as removing/replacing records
*/
return class extends EventSource {
constructor(source, configuration = {}) {
super(configuration => {
const { createEvent, ...superConfiguration } = configuration;
return source
.apply(this, [superConfiguration])
.catch(backoff)
.then(result => {
if (!(result instanceof Error)) {
const _createEvent =
typeof createEvent === 'function' ? createEvent : defaultCreateEvent;
let event = _createEvent(result, configuration);
// allow custom types, but make a default of `message`, ideally this would check for CustomEvent
// but keep this flexible for the moment
if (!event.type) {
event = {
type: 'message',
data: event,
};
}
// meta is also configurable by using createEvent
const meta = get(event.data || {}, 'meta');
if (meta) {
// pick off the `cursor` from the meta and add it to configuration
configuration.cursor = meta.cursor;
}
this.currentEvent = event;
this.dispatchEvent(this.currentEvent);
this.previousEvent = this.currentEvent;
}
return result;
});
}, configuration);
}
// if we are having these props, at least make getters
getCurrentEvent() {
return this.currentEvent;
}
getPreviousEvent() {
return this.previousEvent;
}
};
}

View File

@ -0,0 +1,31 @@
export default function(source, DefaultEventSource, P = Promise) {
return function(sources) {
return function(cb, configuration) {
const key = configuration.key;
if (typeof sources[key] !== 'undefined' && configuration.settings.enabled) {
if (typeof sources[key].configuration === 'undefined') {
sources[key].configuration = {};
}
sources[key].configuration.settings = configuration.settings;
return source(sources[key]);
} else {
const EventSource = configuration.type || DefaultEventSource;
const eventSource = (sources[key] = new EventSource(cb, configuration));
return source(eventSource)
.catch(function(e) {
// any errors, delete from the cache for next time
delete sources[key];
return P.reject(e);
})
.then(function(eventSource) {
// make sure we cancel everything out if there is no cursor
if (typeof eventSource.configuration.cursor === 'undefined') {
eventSource.close();
delete sources[key];
}
return eventSource;
});
}
};
};
}

View File

@ -0,0 +1,70 @@
export const defaultRunner = function(target, configuration, isClosed) {
if (isClosed(target)) {
return;
}
// TODO Consider wrapping this is a promise for none thenable returns
return target.source
.bind(target)(configuration)
.then(function(res) {
return defaultRunner(target, configuration, isClosed);
});
};
const errorEvent = function(e) {
return new ErrorEvent('error', {
error: e,
message: e.message,
});
};
const isClosed = function(target) {
switch (target.readyState) {
case 2: // CLOSED
case 3: // CLOSING
return true;
}
return false;
};
export default function(
EventTarget,
P = Promise,
run = defaultRunner,
createErrorEvent = errorEvent
) {
return class extends EventTarget {
constructor(source, configuration = {}) {
super();
this.readyState = 2;
this.source =
typeof source !== 'function'
? function(configuration) {
this.close();
return P.resolve();
}
: source;
this.readyState = 0; // connecting
P.resolve()
.then(() => {
this.readyState = 1; // open
// ...that the connection _was just_ opened
this.dispatchEvent({ type: 'open' });
return run(this, configuration, isClosed);
})
.catch(e => {
this.dispatchEvent(createErrorEvent(e));
// close after the dispatch so we can tell if it was an error whilst closed or not
// but make sure its before the promise tick
this.readyState = 2; // CLOSE
})
.then(() => {
// This only gets called when the promise chain completely finishes
// so only when its completely closed.
this.readyState = 2; // CLOSE
});
}
close() {
// additional readyState 3 = CLOSING
if (this.readyState !== 2) {
this.readyState = 3;
}
}
};
}

View File

@ -0,0 +1,36 @@
import ObjectProxy from '@ember/object/proxy';
import ArrayProxy from '@ember/array/proxy';
import { Promise } from 'rsvp';
import createListeners from 'consul-ui/utils/dom/create-listeners';
import EventTarget from 'consul-ui/utils/dom/event-target/rsvp';
import cacheFactory from 'consul-ui/utils/dom/event-source/cache';
import proxyFactory from 'consul-ui/utils/dom/event-source/proxy';
import firstResolverFactory from 'consul-ui/utils/dom/event-source/resolver';
import CallableEventSourceFactory from 'consul-ui/utils/dom/event-source/callable';
import ReopenableEventSourceFactory from 'consul-ui/utils/dom/event-source/reopenable';
import BlockingEventSourceFactory from 'consul-ui/utils/dom/event-source/blocking';
import StorageEventSourceFactory from 'consul-ui/utils/dom/event-source/storage';
// All The EventSource-i
export const CallableEventSource = CallableEventSourceFactory(EventTarget, Promise);
export const ReopenableEventSource = ReopenableEventSourceFactory(CallableEventSource);
export const BlockingEventSource = BlockingEventSourceFactory(ReopenableEventSource);
export const StorageEventSource = StorageEventSourceFactory(EventTarget, Promise);
// various utils
export const proxy = proxyFactory(ObjectProxy, ArrayProxy);
export const resolve = firstResolverFactory(Promise);
export const source = function(source) {
// create API needed for conventional promise blocked, loading, Routes
// i.e. resolve/reject on first response
return resolve(source, createListeners()).then(function(data) {
// create API needed for conventional DD/computed and Controllers
return proxy(data, source, createListeners());
});
};
export const cache = cacheFactory(source, BlockingEventSource, Promise);

View File

@ -0,0 +1,50 @@
import { get, set } from '@ember/object';
export default function(ObjProxy, ArrProxy) {
return function(data, source, listeners) {
let Proxy = ObjProxy;
if (typeof data !== 'string' && typeof get(data, 'length') !== 'undefined') {
data = data.filter(function(item) {
return !get(item, 'isDestroyed') && !get(item, 'isDeleted') && get(item, 'isLoaded');
});
}
if (typeof data !== 'string' && typeof get(data, 'length') !== 'undefined') {
Proxy = ArrProxy;
}
const proxy = Proxy.create({
content: data,
init: function() {
this.listeners = listeners;
this.listeners.add(source, 'message', e => {
set(this, 'content', e.data);
});
},
configuration: source.configuration,
addEventListener: function(type, handler) {
// Force use of computed for messages
if (type !== 'message') {
this.listeners.add(source, type, handler);
}
},
getCurrentEvent: function() {
return source.getCurrentEvent(...arguments);
},
removeEventListener: function() {
return source.removeEventListener(...arguments);
},
dispatchEvent: function() {
return source.dispatchEvent(...arguments);
},
close: function() {
return source.close(...arguments);
},
reopen: function() {
return source.reopen(...arguments);
},
willDestroy: function() {
this.listeners.remove();
},
});
return proxy;
};
}

View File

@ -0,0 +1,23 @@
/**
* Wraps an EventSource so that you can `close` and `reopen`
*
* @param {Class} eventSource - EventSource class to extend from
*/
export default function(eventSource = EventSource) {
return class extends eventSource {
constructor(source, configuration) {
super(...arguments);
this.configuration = configuration;
}
reopen() {
switch (this.readyState) {
case 3: // CLOSING
this.readyState = 1;
break;
case 2: // CLOSED
eventSource.apply(this, [this.source, this.configuration]);
break;
}
}
};
}

View File

@ -0,0 +1,29 @@
export default function(P = Promise) {
return function(source, listeners) {
let current;
if (typeof source.getCurrentEvent === 'function') {
current = source.getCurrentEvent();
}
if (current != null) {
// immediately resolve if we have previous cached data
return P.resolve(current.data).then(function(cached) {
source.reopen();
return cached;
});
}
// if we have no previously cached data, listen for the first response
return new P(function(resolve, reject) {
// close, cleanup and reject if we get an error
listeners.add(source, 'error', function(e) {
listeners.remove();
e.target.close();
reject(e.error);
});
// ...or cleanup and respond with the first lot of data
listeners.add(source, 'message', function(e) {
listeners.remove();
resolve(e.data);
});
});
};
}

View File

@ -0,0 +1,40 @@
export default function(EventTarget, P = Promise) {
const handler = function(e) {
if (e.key === this.configuration.key) {
P.resolve(this.getCurrentEvent()).then(event => {
this.configuration.cursor++;
this.dispatchEvent(event);
});
}
};
return class extends EventTarget {
constructor(cb, configuration) {
super(...arguments);
this.source = cb;
this.handler = handler.bind(this);
this.configuration = configuration;
this.configuration.cursor = 1;
this.dispatcher = configuration.dispatcher;
this.reopen();
}
dispatchEvent() {
if (this.readyState === 1) {
return super.dispatchEvent(...arguments);
}
}
close() {
this.dispatcher.removeEventListener('storage', this.handler);
this.readyState = 2;
}
reopen() {
this.dispatcher.addEventListener('storage', this.handler);
this.readyState = 1;
}
getCurrentEvent() {
return {
type: 'message',
data: this.source(this.configuration),
};
}
};
}

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Toru Nagashima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,470 @@
/**
* @typedef {object} PrivateData
* @property {EventTarget} eventTarget The event target.
* @property {{type:string}} event The original event object.
* @property {number} eventPhase The current event phase.
* @property {EventTarget|null} currentTarget The current event target.
* @property {boolean} canceled The flag to prevent default.
* @property {boolean} stopped The flag to stop propagation.
* @property {boolean} immediateStopped The flag to stop propagation immediately.
* @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null.
* @property {number} timeStamp The unix time.
* @private
*/
/**
* Private data for event wrappers.
* @type {WeakMap<Event, PrivateData>}
* @private
*/
const privateData = new WeakMap();
/**
* Cache for wrapper classes.
* @type {WeakMap<Object, Function>}
* @private
*/
const wrappers = new WeakMap();
/**
* Get private data.
* @param {Event} event The event object to get private data.
* @returns {PrivateData} The private data of the event.
* @private
*/
function pd(event) {
const retv = privateData.get(event);
console.assert(retv != null, "'this' is expected an Event object, but got", event);
return retv;
}
/**
* https://dom.spec.whatwg.org/#set-the-canceled-flag
* @param data {PrivateData} private data.
*/
function setCancelFlag(data) {
if (data.passiveListener != null) {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(
'Unable to preventDefault inside passive event listener invocation.',
data.passiveListener
);
}
return;
}
if (!data.event.cancelable) {
return;
}
data.canceled = true;
if (typeof data.event.preventDefault === 'function') {
data.event.preventDefault();
}
}
/**
* @see https://dom.spec.whatwg.org/#interface-event
* @private
*/
/**
* The event wrapper.
* @constructor
* @param {EventTarget} eventTarget The event target of this dispatching.
* @param {Event|{type:string}} event The original event to wrap.
*/
function Event(eventTarget, event) {
privateData.set(this, {
eventTarget,
event,
eventPhase: 2,
currentTarget: eventTarget,
canceled: false,
stopped: false,
immediateStopped: false,
passiveListener: null,
timeStamp: event.timeStamp || Date.now(),
});
// https://heycam.github.io/webidl/#Unforgeable
Object.defineProperty(this, 'isTrusted', { value: false, enumerable: true });
// Define accessors
const keys = Object.keys(event);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
if (!(key in this)) {
Object.defineProperty(this, key, defineRedirectDescriptor(key));
}
}
}
// Should be enumerable, but class methods are not enumerable.
Event.prototype = {
/**
* The type of this event.
* @type {string}
*/
get type() {
return pd(this).event.type;
},
/**
* The target of this event.
* @type {EventTarget}
*/
get target() {
return pd(this).eventTarget;
},
/**
* The target of this event.
* @type {EventTarget}
*/
get currentTarget() {
return pd(this).currentTarget;
},
/**
* @returns {EventTarget[]} The composed path of this event.
*/
composedPath() {
const currentTarget = pd(this).currentTarget;
if (currentTarget == null) {
return [];
}
return [currentTarget];
},
/**
* Constant of NONE.
* @type {number}
*/
get NONE() {
return 0;
},
/**
* Constant of CAPTURING_PHASE.
* @type {number}
*/
get CAPTURING_PHASE() {
return 1;
},
/**
* Constant of AT_TARGET.
* @type {number}
*/
get AT_TARGET() {
return 2;
},
/**
* Constant of BUBBLING_PHASE.
* @type {number}
*/
get BUBBLING_PHASE() {
return 3;
},
/**
* The target of this event.
* @type {number}
*/
get eventPhase() {
return pd(this).eventPhase;
},
/**
* Stop event bubbling.
* @returns {void}
*/
stopPropagation() {
const data = pd(this);
data.stopped = true;
if (typeof data.event.stopPropagation === 'function') {
data.event.stopPropagation();
}
},
/**
* Stop event bubbling.
* @returns {void}
*/
stopImmediatePropagation() {
const data = pd(this);
data.stopped = true;
data.immediateStopped = true;
if (typeof data.event.stopImmediatePropagation === 'function') {
data.event.stopImmediatePropagation();
}
},
/**
* The flag to be bubbling.
* @type {boolean}
*/
get bubbles() {
return Boolean(pd(this).event.bubbles);
},
/**
* The flag to be cancelable.
* @type {boolean}
*/
get cancelable() {
return Boolean(pd(this).event.cancelable);
},
/**
* Cancel this event.
* @returns {void}
*/
preventDefault() {
setCancelFlag(pd(this));
},
/**
* The flag to indicate cancellation state.
* @type {boolean}
*/
get defaultPrevented() {
return pd(this).canceled;
},
/**
* The flag to be composed.
* @type {boolean}
*/
get composed() {
return Boolean(pd(this).event.composed);
},
/**
* The unix time of this event.
* @type {number}
*/
get timeStamp() {
return pd(this).timeStamp;
},
/**
* The target of this event.
* @type {EventTarget}
* @deprecated
*/
get srcElement() {
return pd(this).eventTarget;
},
/**
* The flag to stop event bubbling.
* @type {boolean}
* @deprecated
*/
get cancelBubble() {
return pd(this).stopped;
},
set cancelBubble(value) {
if (!value) {
return;
}
const data = pd(this);
data.stopped = true;
if (typeof data.event.cancelBubble === 'boolean') {
data.event.cancelBubble = true;
}
},
/**
* The flag to indicate cancellation state.
* @type {boolean}
* @deprecated
*/
get returnValue() {
return !pd(this).canceled;
},
set returnValue(value) {
if (!value) {
setCancelFlag(pd(this));
}
},
/**
* Initialize this event object. But do nothing under event dispatching.
* @param {string} type The event type.
* @param {boolean} [bubbles=false] The flag to be possible to bubble up.
* @param {boolean} [cancelable=false] The flag to be possible to cancel.
* @deprecated
*/
initEvent() {
// Do nothing.
},
};
// `constructor` is not enumerable.
Object.defineProperty(Event.prototype, 'constructor', {
value: Event,
configurable: true,
writable: true,
});
// Ensure `event instanceof window.Event` is `true`.
if (typeof window !== 'undefined' && typeof window.Event !== 'undefined') {
Object.setPrototypeOf(Event.prototype, window.Event.prototype);
// Make association for wrappers.
wrappers.set(window.Event.prototype, Event);
}
/**
* Get the property descriptor to redirect a given property.
* @param {string} key Property name to define property descriptor.
* @returns {PropertyDescriptor} The property descriptor to redirect the property.
* @private
*/
function defineRedirectDescriptor(key) {
return {
get() {
return pd(this).event[key];
},
set(value) {
pd(this).event[key] = value;
},
configurable: true,
enumerable: true,
};
}
/**
* Get the property descriptor to call a given method property.
* @param {string} key Property name to define property descriptor.
* @returns {PropertyDescriptor} The property descriptor to call the method property.
* @private
*/
function defineCallDescriptor(key) {
return {
value() {
const event = pd(this).event;
return event[key].apply(event, arguments);
},
configurable: true,
enumerable: true,
};
}
/**
* Define new wrapper class.
* @param {Function} BaseEvent The base wrapper class.
* @param {Object} proto The prototype of the original event.
* @returns {Function} The defined wrapper class.
* @private
*/
function defineWrapper(BaseEvent, proto) {
const keys = Object.keys(proto);
if (keys.length === 0) {
return BaseEvent;
}
/** CustomEvent */
function CustomEvent(eventTarget, event) {
BaseEvent.call(this, eventTarget, event);
}
CustomEvent.prototype = Object.create(BaseEvent.prototype, {
constructor: { value: CustomEvent, configurable: true, writable: true },
});
// Define accessors.
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
if (!(key in BaseEvent.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
const isFunc = typeof descriptor.value === 'function';
Object.defineProperty(
CustomEvent.prototype,
key,
isFunc ? defineCallDescriptor(key) : defineRedirectDescriptor(key)
);
}
}
return CustomEvent;
}
/**
* Get the wrapper class of a given prototype.
* @param {Object} proto The prototype of the original event to get its wrapper.
* @returns {Function} The wrapper class.
* @private
*/
function getWrapper(proto) {
if (proto == null || proto === Object.prototype) {
return Event;
}
let wrapper = wrappers.get(proto);
if (wrapper == null) {
wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto);
wrappers.set(proto, wrapper);
}
return wrapper;
}
/**
* Wrap a given event to management a dispatching.
* @param {EventTarget} eventTarget The event target of this dispatching.
* @param {Object} event The event to wrap.
* @returns {Event} The wrapper instance.
* @private
*/
export function wrapEvent(eventTarget, event) {
const Wrapper = getWrapper(Object.getPrototypeOf(event));
return new Wrapper(eventTarget, event);
}
/**
* Get the immediateStopped flag of a given event.
* @param {Event} event The event to get.
* @returns {boolean} The flag to stop propagation immediately.
* @private
*/
export function isStopped(event) {
return pd(event).immediateStopped;
}
/**
* Set the current event phase of a given event.
* @param {Event} event The event to set current target.
* @param {number} eventPhase New event phase.
* @returns {void}
* @private
*/
export function setEventPhase(event, eventPhase) {
pd(event).eventPhase = eventPhase;
}
/**
* Set the current target of a given event.
* @param {Event} event The event to set current target.
* @param {EventTarget|null} currentTarget New current target.
* @returns {void}
* @private
*/
export function setCurrentTarget(event, currentTarget) {
pd(event).currentTarget = currentTarget;
}
/**
* Set a passive listener of a given event.
* @param {Event} event The event to set current target.
* @param {Function|null} passiveListener New passive listener.
* @returns {void}
* @private
*/
export function setPassiveListener(event, passiveListener) {
pd(event).passiveListener = passiveListener;
}

View File

@ -0,0 +1,63 @@
// Simple RSVP.EventTarget wrapper to make it more like a standard EventTarget
import RSVP from 'rsvp';
// See https://github.com/mysticatea/event-target-shim/blob/v4.0.2/src/event.mjs
// The MIT License (MIT) - Copyright (c) 2015 Toru Nagashima
import { setCurrentTarget, wrapEvent } from './event-target-shim/event';
const EventTarget = function() {};
function callbacksFor(object) {
let callbacks = object._promiseCallbacks;
if (!callbacks) {
callbacks = object._promiseCallbacks = {};
}
return callbacks;
}
EventTarget.prototype = Object.assign(
Object.create(Object.prototype, {
constructor: {
value: EventTarget,
configurable: true,
writable: true,
},
}),
{
dispatchEvent: function(obj) {
// borrow just what I need from event-target-shim
// to make true events even ErrorEvents with targets
const wrappedEvent = wrapEvent(this, obj);
setCurrentTarget(wrappedEvent, null);
// RSVP trigger doesn't bind to `this`
// the rest is pretty much the contents of `trigger`
// but with a `.bind(this)` to make it compatible
// with standard EventTarget
// we use `let` and `callbacksFor` above, just to keep things the same as rsvp.js
const eventName = obj.type;
const options = wrappedEvent;
let allCallbacks = callbacksFor(this);
let callbacks = allCallbacks[eventName];
if (callbacks) {
// Don't cache the callbacks.length since it may grow
let callback;
for (let i = 0; i < callbacks.length; i++) {
callback = callbacks[i];
callback.bind(this)(options);
}
}
},
addEventListener: function(event, cb) {
this.on(event, cb);
},
removeEventListener: function(event, cb) {
try {
this.off(event, cb);
} catch (e) {
// passthrough
}
},
}
);
RSVP.EventTarget.mixin(EventTarget.prototype);
export default EventTarget;

View File

@ -77,9 +77,13 @@ export default function(changeset = defaultChangeset, getFormNameProperty = pars
}
const data = this.getData();
// ember-data/changeset dance
// TODO: This works for ember-data RecordSets and Changesets but not for plain js Objects
// see settings
const json = typeof data.toJSON === 'function' ? data.toJSON() : get(data, 'data').toJSON();
// if the form doesn't include a property then throw so it can be
// caught outside, therefore the user can deal with things that aren't in the data
// TODO: possibly need to add support for deeper properties using `get` here
// for example `client.blocking` instead of just `blocking`
if (!Object.keys(json).includes(prop)) {
const error = new Error(`${prop} property doesn't exist`);
error.target = target;

View File

@ -0,0 +1,34 @@
@setupApplicationTest
Feature: dc / list-blocking
In order to see updates without refreshing the page
As a user
I want to see changes if I change consul externally
Background:
Given 1 datacenter model with the value "dc-1"
And settings from yaml
---
consul:client:
blocking: 1
throttle: 200
---
Scenario:
And 3 [Model] models
And a network latency of 100
When I visit the [Page] page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/[Url]
And pause until I see 3 [Model] models
And an external edit results in 5 [Model] models
And pause until I see 5 [Model] models
And an external edit results in 1 [Model] model
And pause until I see 1 [Model] model
And an external edit results in 0 [Model] models
And pause until I see 0 [Model] models
Where:
--------------------------------------------
| Page | Model | Url |
| services | service | services |
| nodes | node | nodes |
--------------------------------------------

View File

@ -44,7 +44,7 @@ Feature: Page Navigation
| Item | Model | URL | Endpoint | Back |
| service | services | /dc-1/services/service-0 | /v1/health/service/service-0?dc=dc-1 | /dc-1/services |
| node | nodes | /dc-1/nodes/node-0 | /v1/session/node/node-0?dc=dc-1 | /dc-1/nodes |
| kv | kvs | /dc-1/kv/necessitatibus-0/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv |
| kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv |
# | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls |
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/internal/ui/services?dc=dc-1 | /dc-1/intentions |
| token | tokens | /dc-1/acls/tokens/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/policies?dc=dc-1 | /dc-1/acls/tokens |
@ -116,7 +116,7 @@ Feature: Page Navigation
Where:
--------------------------------------------------------------------------------------------------------
| Item | Model | URL | Back |
| kv | kvs | /dc-1/kv/necessitatibus-0/edit | /dc-1/kv |
| kv | kvs | /dc-1/kv/0-key-value/edit | /dc-1/kv |
# | acl | acls | /dc-1/acls/anonymous | /dc-1/acls |
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /dc-1/intentions |
--------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,10 @@
import steps from '../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,84 @@
import domEventSourceCallable from 'consul-ui/utils/dom/event-source/callable';
import EventTarget from 'consul-ui/utils/dom/event-target/rsvp';
import { Promise } from 'rsvp';
import { module } from 'qunit';
import { setupTest } from 'ember-qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Integration | Utility | dom/event-source/callable', function(hooks) {
setupTest(hooks);
test('it dispatches messages', function(assert) {
assert.expect(1);
const EventSource = domEventSourceCallable(EventTarget);
const listener = this.stub();
const source = new EventSource(
function(configuration) {
return new Promise(resolve => {
setTimeout(() => {
this.dispatchEvent({
type: 'message',
data: null,
});
resolve();
}, configuration.milliseconds);
});
},
{
milliseconds: 100,
}
);
source.addEventListener('message', function() {
listener();
});
return new Promise(function(resolve) {
setTimeout(function() {
source.close();
assert.equal(listener.callCount, 5);
resolve();
}, 550);
});
});
test('it dispatches a single open event and closes when called with no callable', function(assert) {
assert.expect(4);
const EventSource = domEventSourceCallable(EventTarget);
const listener = this.stub();
const source = new EventSource();
source.addEventListener('open', function(e) {
assert.deepEqual(e.target, this);
assert.equal(e.target.readyState, 1);
listener();
});
return Promise.resolve().then(function() {
assert.ok(listener.calledOnce);
assert.equal(source.readyState, 2);
});
});
test('it dispatches a single open event, and calls the specified callable that can dispatch an event', function(assert) {
assert.expect(1);
const EventSource = domEventSourceCallable(EventTarget);
const listener = this.stub();
const source = new EventSource(function() {
return new Promise(resolve => {
setTimeout(() => {
this.dispatchEvent({
type: 'message',
data: {},
});
this.close();
}, 190);
});
});
source.addEventListener('open', function() {
// open is called first
listener();
});
return new Promise(function(resolve) {
source.addEventListener('message', function() {
// message is called second
assert.ok(listener.calledOnce);
resolve();
});
});
});
});

View File

@ -65,7 +65,10 @@ export default function(assert) {
}, yadda)
)
// doubles
.given(['$number $model model[s]?', '$number $model models'], function(number, model) {
.given(['an external edit results in $number $model model[s]?'], function(number, model) {
return create(number, model);
})
.given(['$number $model model[s]?'], function(number, model) {
return create(number, model);
})
.given(['$number $model model[s]? with the value "$value"'], function(number, model, value) {
@ -77,7 +80,15 @@ export default function(assert) {
return create(number, model, data);
}
)
.given(["I'm using a legacy token"], function(number, model, data) {
.given(['settings from yaml\n$yaml'], function(data) {
return Object.keys(data).forEach(function(key) {
window.localStorage[key] = JSON.stringify(data[key]);
});
})
.given('a network latency of $number', function(number) {
api.server.setCookie('CONSUL_LATENCY', number);
})
.given(["I'm using a legacy token"], function() {
window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' });
})
// TODO: Abstract this away from HTTP
@ -188,6 +199,26 @@ export default function(assert) {
});
})
// assertions
.then('pause until I see $number $model model[s]?', function(num, model) {
return new Promise(function(resolve) {
let count = 0;
const interval = setInterval(function() {
if (++count >= 50) {
clearInterval(interval);
assert.ok(false);
resolve();
}
const len = currentPage[`${pluralize(model)}`].filter(function(item) {
return item.isVisible;
}).length;
if (len === num) {
clearInterval(interval);
assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`);
resolve();
}
}, 100);
});
})
.then('a $method request is made to "$url" with the body from yaml\n$yaml', function(
method,
url,
@ -358,6 +389,9 @@ export default function(assert) {
.then('I have settings like yaml\n$yaml', function(data) {
// TODO: Inject this
const settings = window.localStorage;
// TODO: this and the setup should probably use consul:
// as we are talking about 'settings' here not localStorage
// so the prefix should be hidden
Object.keys(data).forEach(function(prop) {
const actual = settings.getItem(prop);
const expected = data[prop];

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:settings', 'Unit | Controller | settings', {
// Specify the other units that are required for this test.
needs: ['service:settings', 'service:dom'],
});
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.subject();
assert.ok(controller);
});

View File

@ -3,6 +3,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('route:settings', 'Unit | Route | settings', {
// Specify the other units that are required for this test.
needs: [
'service:client/http',
'service:repository/dc',
'service:settings',
'service:logger',

View File

@ -0,0 +1,87 @@
import domEventSourceBlocking, {
create5xxBackoff,
} from 'consul-ui/utils/dom/event-source/blocking';
import { module } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Unit | Utility | dom/event-source/blocking');
const createEventSource = function() {
return class {
constructor(cb) {
this.readyState = 1;
this.source = cb;
this.source.apply(this, arguments);
}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {}
close() {}
};
};
const createPromise = function(resolve = function() {}) {
class PromiseMock {
constructor(cb = function() {}) {
cb(resolve);
}
then(cb) {
setTimeout(() => cb.bind(this)(), 0);
return this;
}
catch(cb) {
cb({ message: 'error' });
return this;
}
}
PromiseMock.resolve = function() {
return new PromiseMock();
};
return PromiseMock;
};
test('it creates an BlockingEventSource class implementing EventSource', function(assert) {
const EventSource = createEventSource();
const BlockingEventSource = domEventSourceBlocking(EventSource, function() {});
assert.ok(BlockingEventSource instanceof Function);
const source = new BlockingEventSource(function() {
return createPromise().resolve();
});
assert.ok(source instanceof EventSource);
});
test("the 5xx backoff continues to throw when it's not a 5xx", function(assert) {
const backoff = create5xxBackoff();
[
undefined,
null,
new Error(),
{ errors: [] },
{ errors: [{ status: '0' }] },
{ errors: [{ status: 501 }] },
{ errors: [{ status: '401' }] },
{ errors: [{ status: '500' }] },
{ errors: [{ status: '5' }] },
{ errors: [{ status: '50' }] },
{ errors: [{ status: '5000' }] },
{ errors: [{ status: '5050' }] },
].forEach(function(item) {
assert.throws(function() {
backoff(item);
});
});
});
test('the 5xx backoff returns a resolve promise on a 5xx (apart from 500)', function(assert) {
[
{ errors: [{ status: '501' }] },
{ errors: [{ status: '503' }] },
{ errors: [{ status: '504' }] },
{ errors: [{ status: '524' }] },
].forEach(item => {
const timeout = this.stub().callsArg(0);
const resolve = this.stub().withArgs(item);
const Promise = createPromise(resolve);
const backoff = create5xxBackoff(undefined, Promise, timeout);
const promise = backoff(item);
assert.ok(promise instanceof Promise, 'a promise was returned');
assert.ok(resolve.calledOnce, 'the promise was resolved with the correct arguments');
assert.ok(timeout.calledOnce, 'timeout was called once');
});
});

View File

@ -0,0 +1,145 @@
import domEventSourceCache from 'consul-ui/utils/dom/event-source/cache';
import { module } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Unit | Utility | dom/event-source/cache');
const createEventSource = function() {
return class {
constructor(cb) {
this.source = cb;
this.source.apply(this, arguments);
}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {}
close() {}
};
};
const createPromise = function(
resolve = result => result,
reject = (result = { message: 'error' }) => result
) {
class PromiseMock {
constructor(cb = function() {}) {
cb(resolve);
}
then(cb) {
setTimeout(() => cb.bind(this)(resolve()), 0);
return this;
}
catch(cb) {
setTimeout(() => cb.bind(this)(reject()), 0);
return this;
}
}
PromiseMock.resolve = function(result) {
return new PromiseMock(function(resolve) {
resolve(result);
});
};
PromiseMock.reject = function() {
return new PromiseMock();
};
return PromiseMock;
};
test('it returns a function', function(assert) {
const EventSource = createEventSource();
const Promise = createPromise();
const getCache = domEventSourceCache(function() {}, EventSource, Promise);
assert.ok(typeof getCache === 'function');
});
test('getCache returns a function', function(assert) {
const EventSource = createEventSource();
const Promise = createPromise();
const getCache = domEventSourceCache(function() {}, EventSource, Promise);
const obj = {};
const cache = getCache(obj);
assert.ok(typeof cache === 'function');
});
test('cache creates the default EventSource and keeps it open when there is a cursor', function(assert) {
const EventSource = createEventSource();
const stub = {
configuration: { cursor: 1 },
};
const Promise = createPromise(function() {
return stub;
});
const source = this.stub().returns(Promise.resolve());
const cb = this.stub();
const getCache = domEventSourceCache(source, EventSource, Promise);
const obj = {};
const cache = getCache(obj);
const promisedEventSource = cache(cb, {
key: 'key',
settings: {
enabled: true,
},
});
assert.ok(source.calledOnce, 'promisifying source called once');
assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise');
const retrievedEventSource = cache(cb, {
key: 'key',
settings: {
enabled: true,
},
});
assert.deepEqual(promisedEventSource, retrievedEventSource);
assert.ok(source.calledTwice, 'promisifying source called once');
assert.ok(retrievedEventSource instanceof Promise, 'source returns a Promise');
});
test('cache creates the default EventSource and keeps it open when there is a cursor', function(assert) {
const EventSource = createEventSource();
const stub = {
close: this.stub(),
configuration: { cursor: 1 },
};
const Promise = createPromise(function() {
return stub;
});
const source = this.stub().returns(Promise.resolve());
const cb = this.stub();
const getCache = domEventSourceCache(source, EventSource, Promise);
const obj = {};
const cache = getCache(obj);
const promisedEventSource = cache(cb, {
key: 0,
settings: {
enabled: true,
},
});
assert.ok(source.calledOnce, 'promisifying source called once');
assert.ok(cb.calledOnce, 'callable event source callable called once');
assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise');
// >>
return promisedEventSource.then(function() {
assert.notOk(stub.close.called, "close wasn't called");
});
});
test("cache creates the default EventSource and closes it when there isn't a cursor", function(assert) {
const EventSource = createEventSource();
const stub = {
close: this.stub(),
configuration: {},
};
const Promise = createPromise(function() {
return stub;
});
const source = this.stub().returns(Promise.resolve());
const cb = this.stub();
const getCache = domEventSourceCache(source, EventSource, Promise);
const obj = {};
const cache = getCache(obj);
const promisedEventSource = cache(cb, {
key: 0,
});
assert.ok(source.calledOnce, 'promisifying source called once');
assert.ok(cb.calledOnce, 'callable event source callable called once');
assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise');
// >>
return promisedEventSource.then(function() {
assert.ok(stub.close.calledOnce, 'close was called');
});
});

View File

@ -0,0 +1,65 @@
import domEventSourceCallable, { defaultRunner } from 'consul-ui/utils/dom/event-source/callable';
import { module } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Unit | Utility | dom/event-source/callable');
const createEventTarget = function() {
return class {
addEventListener() {}
removeEventListener() {}
dispatchEvent() {}
};
};
const createPromise = function() {
class PromiseMock {
then(cb) {
cb();
return this;
}
catch(cb) {
cb({ message: 'error' });
return this;
}
}
PromiseMock.resolve = function() {
return new PromiseMock();
};
return PromiseMock;
};
test('it creates an EventSource class implementing EventTarget', function(assert) {
const EventTarget = createEventTarget();
const EventSource = domEventSourceCallable(EventTarget, createPromise());
assert.ok(EventSource instanceof Function);
const source = new EventSource();
assert.ok(source instanceof EventTarget);
});
test('the default runner loops and can be closed', function(assert) {
assert.expect(12); // 10 not closed, 1 to close and the final call count
let count = 0;
const isClosed = function() {
count++;
assert.ok(true);
return count === 11;
};
const configuration = {};
const then = this.stub().callsArg(0);
const target = {
source: function(configuration) {
return {
then: then,
};
},
};
defaultRunner(target, configuration, isClosed);
assert.ok(then.callCount == 10);
});
test('it calls the defaultRunner', function(assert) {
const Promise = createPromise();
const EventTarget = createEventTarget();
const run = this.stub();
const EventSource = domEventSourceCallable(EventTarget, Promise, run);
const source = new EventSource();
assert.ok(run.calledOnce);
assert.equal(source.readyState, 2);
});

View File

@ -0,0 +1,28 @@
import {
source,
proxy,
cache,
resolve,
CallableEventSource,
ReopenableEventSource,
BlockingEventSource,
StorageEventSource,
} from 'consul-ui/utils/dom/event-source/index';
import { module, test } from 'qunit';
module('Unit | Utility | dom/event source/index');
// Replace this with your real tests.
test('it works', function(assert) {
// All The EventSource
assert.ok(typeof CallableEventSource === 'function');
assert.ok(typeof ReopenableEventSource === 'function');
assert.ok(typeof BlockingEventSource === 'function');
assert.ok(typeof StorageEventSource === 'function');
// Utils
assert.ok(typeof source === 'function');
assert.ok(typeof proxy === 'function');
assert.ok(typeof cache === 'function');
assert.ok(typeof resolve === 'function');
});

View File

@ -0,0 +1,10 @@
import domEventSourceProxy from 'consul-ui/utils/dom/event-source/proxy';
import { module, test } from 'qunit';
module('Unit | Utility | dom/event source/proxy');
// Replace this with your real tests.
test('it works', function(assert) {
let result = domEventSourceProxy();
assert.ok(result);
});

View File

@ -0,0 +1,46 @@
import domEventSourceReopenable from 'consul-ui/utils/dom/event-source/reopenable';
import { module } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Unit | Utility | dom/event-source/reopenable');
const createEventSource = function() {
return class {
constructor(cb) {
this.readyState = 1;
this.source = cb;
this.source.apply(this, arguments);
}
addEventListener() {}
removeEventListener() {}
dispatchEvent() {}
close() {}
};
};
test('it creates an Reopenable class implementing EventSource', function(assert) {
const EventSource = createEventSource();
const ReopenableEventSource = domEventSourceReopenable(EventSource);
assert.ok(ReopenableEventSource instanceof Function);
const source = new ReopenableEventSource(function() {});
assert.ok(source instanceof EventSource);
});
test('it reopens the event source when reopen is called', function(assert) {
const callable = this.stub();
const EventSource = createEventSource();
const ReopenableEventSource = domEventSourceReopenable(EventSource);
const source = new ReopenableEventSource(callable);
assert.equal(source.readyState, 1);
// first automatic EventSource `open`
assert.ok(callable.calledOnce);
source.readyState = 3;
source.reopen();
// still only called once as it hasn't completely closed yet
// therefore is just opened by resetting the readyState
assert.ok(callable.calledOnce);
assert.equal(source.readyState, 1);
// properly close the source
source.readyState = 2;
source.reopen();
// this time it is reopened via a recall of the callable
assert.ok(callable.calledTwice);
});

View File

@ -0,0 +1,10 @@
import domEventSourceResolver from 'consul-ui/utils/dom/event-source/resolver';
import { module, test } from 'qunit';
module('Unit | Utility | dom/event source/resolver');
// Replace this with your real tests.
test('it works', function(assert) {
let result = domEventSourceResolver();
assert.ok(result);
});

View File

@ -0,0 +1,10 @@
import domEventSourceStorage from 'consul-ui/utils/dom/event-source/storage';
import { module, test } from 'qunit';
module('Unit | Utility | dom/event source/storage');
// Replace this with your real tests.
test('it works', function(assert) {
let result = domEventSourceStorage(function EventTarget() {});
assert.ok(result);
});

View File

@ -0,0 +1,13 @@
import domEventTargetRsvp from 'consul-ui/utils/dom/event-target/rsvp';
import { module, test } from 'qunit';
module('Unit | Utility | dom/event-target/rsvp');
// Replace this with your real tests.
test('it has EventTarget methods', function(assert) {
const result = domEventTargetRsvp;
assert.equal(typeof result, 'function');
['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(function(item) {
assert.equal(typeof result.prototype[item], 'function');
});
});

View File

@ -543,6 +543,7 @@
"@gardenhq/component-factory@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@gardenhq/component-factory/-/component-factory-1.4.0.tgz#f5da8ddf2050fde9c69f4426d61fe55de043e78d"
integrity sha1-9dqN3yBQ/enGn0Qm1h/lXeBD540=
dependencies:
"@gardenhq/domino" "^1.0.0"
"@gardenhq/tick-control" "^2.0.0"
@ -552,6 +553,7 @@
"@gardenhq/domino@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gardenhq/domino/-/domino-1.0.0.tgz#832c493f3f05697b7df4ccce00c4cf620dc60923"
integrity sha1-gyxJPz8FaXt99MzOAMTPYg3GCSM=
optionalDependencies:
min-document "^2.19.0"
unfetch "^2.1.2"
@ -560,6 +562,7 @@
"@gardenhq/o@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@gardenhq/o/-/o-8.0.1.tgz#d6772cec7e4295a951165284cf43fbd0a373b779"
integrity sha1-1ncs7H5ClalRFlKEz0P70KNzt3k=
dependencies:
"@gardenhq/component-factory" "^1.4.0"
"@gardenhq/tick-control" "^2.0.0"
@ -577,10 +580,12 @@
"@gardenhq/tick-control@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@gardenhq/tick-control/-/tick-control-2.0.0.tgz#f84fe38ca7a09b7b2b52f42945c50429ba639897"
integrity sha1-+E/jjKegm3srUvQpRcUEKbpjmJc=
"@gardenhq/willow@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@gardenhq/willow/-/willow-6.2.0.tgz#3e4bc220a89099732746ead3385cc097bfb70186"
integrity sha1-PkvCIKiQmXMnRurTOFzAl7+3AYY=
"@glimmer/di@^0.2.0":
version "0.2.1"
@ -593,8 +598,9 @@
"@glimmer/di" "^0.2.0"
"@hashicorp/api-double@^1.3.0":
version "1.4.4"
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.4.tgz#db5521230b0031bfc3dc3cc5b775f17413a4fe91"
version "1.4.5"
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.5.tgz#839ba882fad76eb17fd2eb3a8899bf5dd5a162a8"
integrity sha512-X8xRtZGXu4JAlh/deaaPW15L8gJIqwNpVEM2OKLkQu1AWHXSh3NF8Vhd5U81061+Dha8Ohl8aEE7LZ8f1tPvzg==
dependencies:
"@gardenhq/o" "^8.0.1"
"@gardenhq/tick-control" "^2.0.0"
@ -606,12 +612,14 @@
js-yaml "^3.10.0"
"@hashicorp/consul-api-double@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.0.1.tgz#eaf2e3f230fbdd876c90b931fd4bb4d94aac10e2"
version "2.1.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.1.0.tgz#511e6a48842ad31133e2070f3b2307568539b10e"
integrity sha512-cyW7TiKQylrWzVUORT1e6m4SU8tQ1V5BYEKW2th7QwHP8OFazn/+om9hud/9X5YtjEuSPIQCmFIvhEVwZgLVpQ==
"@hashicorp/ember-cli-api-double@^1.3.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.7.0.tgz#4fdab6152157dd82b999de030c593c87e0cdb8b7"
integrity sha512-ojPcUPyId+3hTbwAtBGYbP5TfCGVAH8Ky6kH+BzlisIO/8XKURo9BSYnFtmYWLgXQVLOIE3iuoia5kOjGS/w2A==
dependencies:
"@hashicorp/api-double" "^1.3.0"
array-range "^1.0.1"
@ -769,6 +777,7 @@
"@xg-wang/whatwg-fetch@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@xg-wang/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#f7b222c012a238e7d6e89ed3d72a1e0edb58453d"
integrity sha512-ULtqA6L75RLzTNW68IiOja0XYv4Ebc3OGMzfia1xxSEMpD0mk/pMvkQX0vbCFyQmKc5xGp80Ms2WiSlXLh8hbA==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
@ -995,6 +1004,7 @@ are-we-there-yet@~1.1.2:
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
@ -1031,6 +1041,7 @@ array-find-index@^1.0.1:
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
array-map@~0.0.0:
version "0.0.0"
@ -1039,6 +1050,7 @@ array-map@~0.0.0:
array-range@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/array-range/-/array-range-1.0.1.tgz#f56e46591843611c6a56f77ef02eda7c50089bfc"
integrity sha1-9W5GWRhDYRxqVvd+8C7afFAIm/w=
array-reduce@~0.0.0:
version "0.0.0"
@ -1745,6 +1757,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
babel-standalone@^6.24.2:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886"
integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY=
babel-template@^6.24.1, babel-template@^6.26.0:
version "6.26.0"
@ -1903,6 +1916,7 @@ body-parser@1.18.2:
body-parser@1.18.3, body-parser@^1.18.3:
version "1.18.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=
dependencies:
bytes "3.0.0"
content-type "~1.0.4"
@ -2640,6 +2654,7 @@ bytes@1:
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
cacache@^10.0.4:
version "10.0.4"
@ -2895,6 +2910,7 @@ class-utils@^0.3.5:
classtrophobic-es5@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/classtrophobic-es5/-/classtrophobic-es5-0.2.1.tgz#9bbfa62a9928abf26f385440032fb49da1cda88f"
integrity sha1-m7+mKpkoq/JvOFRAAy+0naHNqI8=
clean-base-url@^1.0.0:
version "1.0.0"
@ -3091,6 +3107,7 @@ commander@^2.6.0:
commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==
commander@~2.17.1:
version "2.17.1"
@ -3206,10 +3223,12 @@ constants-browserify@^1.0.0, constants-browserify@~1.0.0:
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
continuable-cache@^0.3.1:
version "0.3.1"
@ -3232,6 +3251,7 @@ convert-source-map@~1.1.0:
cookie-parser@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5"
integrity sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=
dependencies:
cookie "0.3.1"
cookie-signature "1.0.6"
@ -3239,10 +3259,12 @@ cookie-parser@^1.4.3:
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
copy-concurrently@^1.0.0:
version "1.0.5"
@ -3601,6 +3623,7 @@ des.js@^1.0.0:
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
detect-file@^0.1.0:
version "0.1.0"
@ -3657,6 +3680,7 @@ dom-serializer@0:
dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
domain-browser@^1.1.1:
version "1.2.0"
@ -3716,6 +3740,7 @@ editions@^1.1.1:
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47:
version "1.3.62"
@ -4701,6 +4726,7 @@ emojis-list@^2.0.0:
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
encoding@^0.1.11:
version "0.1.12"
@ -4866,6 +4892,7 @@ es6-weak-map@^2.0.1:
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
@ -4958,6 +4985,7 @@ espree@^3.5.4:
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esprima@~3.0.0:
version "3.0.0"
@ -4994,6 +5022,7 @@ esutils@^2.0.2:
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-emitter@~0.3.5:
version "0.3.5"
@ -5189,6 +5218,7 @@ express@^4.10.7, express@^4.12.3:
express@^4.16.2:
version "4.16.4"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e"
integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==
dependencies:
accepts "~1.3.5"
array-flatten "1.1.1"
@ -5292,10 +5322,12 @@ eyes@0.1.x:
fake-xml-http-request@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.0.0.tgz#41a92f0ca539477700cb1dafd2df251d55dac8ff"
integrity sha512-UjNnynb6eLAB0lyh2PlTEkjRJORnNsVF1hbzU+PQv89/cyBV9GDRCy7JAcLQgeCLYT+3kaumWWZKEJvbaK74eQ==
faker@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=
fast-deep-equal@^1.0.0:
version "1.1.0"
@ -5372,7 +5404,8 @@ file-entry-cache@^2.0.0:
file-saver@^1.3.3:
version "1.3.8"
resolved "http://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==
filename-regex@^2.0.0:
version "2.0.1"
@ -5410,7 +5443,8 @@ fill-range@^4.0.0:
finalhandler@1.1.1:
version "1.1.1"
resolved "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
@ -5552,6 +5586,7 @@ formatio@1.2.0:
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fragment-cache@^0.2.1:
version "0.2.1"
@ -5562,6 +5597,7 @@ fragment-cache@^0.2.1:
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
from2@^2.1.0:
version "2.3.0"
@ -6200,7 +6236,8 @@ http-errors@1.6.2:
http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
version "1.6.3"
resolved "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
dependencies:
depd "~1.1.2"
inherits "2.0.3"
@ -6260,6 +6297,7 @@ husky@^1.1.0:
hyperhtml@^0.15.5:
version "0.15.10"
resolved "https://registry.yarnpkg.com/hyperhtml/-/hyperhtml-0.15.10.tgz#5e5f42393d4fc30cd803063fb88a5c9d97625e1c"
integrity sha512-D3dkc5nac47dzGXhLfGTearEoUXLk8ijSrj+5ngEH1Od+6EZ9Cwjspj/MWWx74DWpvCH+glO7M+B7WqCYSzkTg==
iconv-lite@0.4.19:
version "0.4.19"
@ -6268,6 +6306,7 @@ iconv-lite@0.4.19:
iconv-lite@0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
@ -6431,6 +6470,7 @@ invert-kv@^1.0.0:
ipaddr.js@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4=
is-accessor-descriptor@^0.1.6:
version "0.1.6"
@ -6631,6 +6671,7 @@ is-path-inside@^1.0.0:
is-plain-obj@^1.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
@ -6862,7 +6903,15 @@ js-yaml@0.3.x:
version "0.3.7"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62"
js-yaml@^3.10.0, js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.8.4:
js-yaml@^3.10.0, js-yaml@^3.11.0, js-yaml@^3.8.4:
version "3.12.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600"
integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.12.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
dependencies:
@ -7701,7 +7750,8 @@ mdurl@^1.0.1:
media-typer@0.3.0:
version "0.3.0"
resolved "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
mem@^1.1.0:
version "1.1.0"
@ -7740,10 +7790,12 @@ meow@^3.4.0, meow@^3.7.0:
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-1.0.1.tgz#2a64b24457becd4e4dc608283247e94ce589aa32"
integrity sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==
dependencies:
is-plain-obj "^1.1"
@ -7772,6 +7824,7 @@ merge@^1.1.3:
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7:
version "2.3.11"
@ -7827,6 +7880,7 @@ mime-db@~1.36.0:
mime-db@~1.37.0:
version "1.37.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.7:
version "2.1.20"
@ -7843,12 +7897,14 @@ mime-types@^2.1.18:
mime-types@~2.1.18:
version "2.1.21"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
dependencies:
mime-db "~1.37.0"
mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
mimic-fn@^1.0.0:
version "1.2.0"
@ -7857,6 +7913,7 @@ mimic-fn@^1.0.0:
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
dependencies:
dom-walk "^0.1.0"
@ -7972,6 +8029,7 @@ morgan@^1.8.1:
mousetrap@^1.6.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587"
integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA==
mout@^1.0.0:
version "1.1.0"
@ -7991,6 +8049,7 @@ move-concurrently@^1.0.1:
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.1.1:
version "2.1.1"
@ -8051,7 +8110,8 @@ natural-compare@^1.4.0:
ncp@^2.0.0:
version "2.0.0"
resolved "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
needle@^2.2.1:
version "2.2.4"
@ -8064,6 +8124,7 @@ needle@^2.2.1:
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
neo-async@^2.5.0:
version "2.5.2"
@ -8409,6 +8470,7 @@ object.values@^1.0.4:
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
@ -8607,6 +8669,7 @@ parseuri@0.0.5:
parseurl@~1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
pascalcase@^0.1.1:
version "0.1.1"
@ -8663,6 +8726,7 @@ path-posix@^1.0.0:
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.7.0"
@ -8783,6 +8847,7 @@ preserve@^0.2.0:
pretender@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.1.1.tgz#5085f0a1272c31d5b57c488386f69e6ca207cb35"
integrity sha512-IkidsJzaroAanw3I43tKCFm2xCpurkQr9aPXv5/jpN+LfCwDaeI8rngVWtQZTx4qqbhc5zJspnLHJ4N/25KvDQ==
dependencies:
"@xg-wang/whatwg-fetch" "^3.0.0"
fake-xml-http-request "^2.0.0"
@ -8974,6 +9039,7 @@ randomfill@^1.0.3:
range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
raw-body@2.3.2:
version "2.3.2"
@ -8987,6 +9053,7 @@ raw-body@2.3.2:
raw-body@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==
dependencies:
bytes "3.0.0"
http-errors "1.6.3"
@ -9115,6 +9182,7 @@ recast@^0.11.3:
recursive-readdir-sync@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz#1dbf6d32f3c5bb8d3cde97a6c588d547a9e13d56"
integrity sha1-Hb9tMvPFu4083pemxYjVR6nhPVY=
redent@^1.0.0:
version "1.0.0"
@ -9436,6 +9504,7 @@ rollup-plugin-commonjs@^9.1.0:
rollup-plugin-memory@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-memory/-/rollup-plugin-memory-2.0.0.tgz#0a8ac6b57fa0e714f89a15c3ac82bc93f89c47c5"
integrity sha1-CorGtX+g5xT4mhXDrIK8k/icR8U=
rollup-plugin-node-resolve@^3.3.0:
version "3.4.0"
@ -9468,6 +9537,7 @@ rollup@^0.59.0:
route-recognizer@^0.3.3:
version "0.3.4"
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"
integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==
rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1, rsvp@^3.3.3, rsvp@^3.5.0:
version "3.6.2"
@ -9550,6 +9620,7 @@ safe-regex@^1.1.0:
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
samsam@1.3.0, samsam@1.x, samsam@^1.1.3:
version "1.3.0"
@ -9619,6 +9690,7 @@ semver@~5.3.0:
send@0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==
dependencies:
debug "2.6.9"
depd "~1.1.2"
@ -9641,6 +9713,7 @@ serialize-javascript@^1.4.0:
serve-static@1.13.2:
version "1.13.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
@ -9684,6 +9757,7 @@ setprototypeof@1.0.3:
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
version "2.4.11"
@ -9894,6 +9968,7 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4:
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
@ -9960,6 +10035,7 @@ sprintf-js@^1.0.3:
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sri-toolbox@^0.2.0:
version "0.2.0"
@ -10012,6 +10088,7 @@ static-extend@^0.1.1:
statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
stdout-stream@^1.4.0:
version "1.4.1"
@ -10608,6 +10685,7 @@ underscore@~1.6.0:
unfetch@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-2.1.2.tgz#684fee4d8acdb135bdb26c0364c642fc326ca95b"
integrity sha1-aE/uTYrNsTW9smwDZMZC/DJsqVs=
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
@ -10662,6 +10740,7 @@ universalify@^0.1.0:
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
unquote@~1.1.1:
version "1.1.1"
@ -10758,6 +10837,7 @@ util@^0.10.3:
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2:
version "3.3.2"
@ -10779,6 +10859,7 @@ validate-npm-package-name@^3.0.0:
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
verror@1.10.0:
version "1.10.0"
@ -11004,6 +11085,7 @@ xdg-basedir@^3.0.0:
xhr2@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8=
xmldom@^0.1.19:
version "0.1.27"