-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI: EventSource project implementation to enable blocking queries for service and node listings #5267
UI: EventSource project implementation to enable blocking queries for service and node listings #5267
Changes from 5 commits
9d798ae
c387b88
6df68a8
85bc7c9
dcd21af
43ac7df
3407887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The above uses the controller-lifecycle PR (#5056) to automatically cleanup any 'closeable' things, in this case EventSources |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
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) { | ||
(prop => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wrap this in an iife? Wouldn't removing the iife result in the same thing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm yeah , can't remember exactly. I think it's left over from a different version of that that had some async stuff in there somewhere. I vaguely remember putting this in as I hit that |
||
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); | ||
}; | ||
} | ||
} | ||
})(prop); | ||
} | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
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; | ||
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 (settings.blocking) { | ||
// ...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 (!settings.blocking) { | ||
// 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 | ||
} | ||
throw e; | ||
}); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The above The code around it gives us the opportunity to centrally deal with a post-initialization on/off setting, plus any other settings or global EventSource related errors. Please note, we're planning on removing the |
||
// 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 (configuration.cursor !== 'undefined' && settings.throttle) { | ||
return throttle(settings.throttle).then(cb); | ||
} | ||
return cb(); | ||
}, | ||
{ | ||
key: key, | ||
type: BlockingEventSource, | ||
} | ||
); | ||
}; | ||
}; | ||
let cache = null; | ||
export default LazyProxyService.extend({ | ||
store: service('store'), | ||
settings: service('settings'), | ||
wait: service('timeout'), | ||
init: function() { | ||
this._super(...arguments); | ||
if (cache === null) { | ||
cache = createCache({}); | ||
} | ||
}, | ||
willDestroy: function() { | ||
cache = null; | ||
}, | ||
shouldProxy: function(content, method) { | ||
return method.indexOf('find') === 0; | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any |
||
execute: function(repo, find) { | ||
return get(this, 'settings') | ||
.findBySlug('client') | ||
.then(settings => { | ||
return createProxy.bind(this)(repo, find, settings, cache); | ||
}); | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is neat. |
||
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 | | ||
-------------------------------------------- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below here we dynamically create the new EventSource returning repositories. Right now we just need to do this with nodes and services - other models will be added in here to enable blocking queries support for those models.