client/events: improve event dispatching
This commit introduces timer-less retry system: 1. Any change to URL is going to stop listening to any messages. 2. If a message is sent and there's no handler that could pick it up, the message gets enqueued. 3. The message is sent again to the first handler that attaches itself to given event type. While in theory this is full of holes (no control over the first handler), in practice, it works quite well. Additionally, views.listenToMessages was attaching to completely wrong DOM node; this commit fixes this as well.
This commit is contained in:
parent
09bc5f10f9
commit
2a4241641c
23 changed files with 133 additions and 100 deletions
|
@ -1,5 +1,5 @@
|
|||
<div class='pager'>
|
||||
<div class='page-header-holder'></div>
|
||||
<div class='pages-holder'></div>
|
||||
<div class='messages'></div>
|
||||
<div class='pages-holder'></div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ class PageController {
|
|||
constructor() {
|
||||
events.listen(events.SettingsChange, () => {
|
||||
this.update();
|
||||
return true;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class TagsController {
|
|||
Promise.all(promises).then(
|
||||
() => {
|
||||
events.notify(events.TagsChange);
|
||||
events.notify(events.Success, 'Changes saved successfully');
|
||||
events.notify(events.Success, 'Changes saved.');
|
||||
},
|
||||
response => {
|
||||
events.notify(events.Error, response.description);
|
||||
|
|
|
@ -43,7 +43,9 @@ class TopNavController {
|
|||
this.topNavView.activate(this.activeItem);
|
||||
};
|
||||
|
||||
events.listen(events.Authentication, rerender);
|
||||
events.listen(
|
||||
events.Authentication,
|
||||
() => { rerender(); return true; });
|
||||
rerender();
|
||||
}
|
||||
|
||||
|
|
|
@ -195,25 +195,23 @@ class UsersController {
|
|||
|
||||
_delete(user) {
|
||||
const isLoggedIn = api.isLoggedIn(user);
|
||||
return new Promise((resolve, reject) => {
|
||||
api.delete('/user/' + user.name)
|
||||
.then(response => {
|
||||
if (isLoggedIn) {
|
||||
api.forget();
|
||||
api.logout();
|
||||
}
|
||||
resolve();
|
||||
if (api.hasPrivilege('users:list')) {
|
||||
page('/users');
|
||||
} else {
|
||||
page('/');
|
||||
}
|
||||
events.notify(events.Success, 'Account deleted');
|
||||
}, response => {
|
||||
reject();
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
});
|
||||
return api.delete('/user/' + user.name)
|
||||
.then(response => {
|
||||
if (isLoggedIn) {
|
||||
api.forget();
|
||||
api.logout();
|
||||
}
|
||||
if (api.hasPrivilege('users:list')) {
|
||||
page('/users');
|
||||
} else {
|
||||
page('/');
|
||||
}
|
||||
events.notify(events.Success, 'Account deleted.');
|
||||
return Promise.resolve();
|
||||
}, response => {
|
||||
events.notify(events.Error, response.description);
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
_show(user, section) {
|
||||
|
|
|
@ -1,34 +1,48 @@
|
|||
'use strict';
|
||||
|
||||
let listeners = [];
|
||||
let pendingMessages = new Map();
|
||||
let listeners = new Map();
|
||||
|
||||
function unlisten(messageClass) {
|
||||
listeners[messageClass] = [];
|
||||
listeners.set(messageClass, []);
|
||||
}
|
||||
|
||||
function listen(messageClass, handler) {
|
||||
if (!(messageClass in listeners)) {
|
||||
listeners[messageClass] = [];
|
||||
if (pendingMessages.has(messageClass)) {
|
||||
let newPendingMessages = [];
|
||||
for (let message of pendingMessages.get(messageClass)) {
|
||||
if (!handler(message)) {
|
||||
newPendingMessages.push(message);
|
||||
}
|
||||
}
|
||||
pendingMessages.set(messageClass, newPendingMessages);
|
||||
}
|
||||
listeners[messageClass].push(handler);
|
||||
if (!listeners.has(messageClass)) {
|
||||
listeners.set(messageClass, []);
|
||||
}
|
||||
listeners.get(messageClass).push(handler);
|
||||
}
|
||||
|
||||
function notify(messageClass, message) {
|
||||
if (!(messageClass in listeners)) {
|
||||
if (!listeners.has(messageClass) || !listeners.get(messageClass).length) {
|
||||
if (!pendingMessages.has(messageClass)) {
|
||||
pendingMessages.set(messageClass, []);
|
||||
}
|
||||
pendingMessages.get(messageClass).push(message);
|
||||
return;
|
||||
}
|
||||
for (let handler of listeners[messageClass]) {
|
||||
for (let handler of listeners.get(messageClass)) {
|
||||
handler(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Success: 1,
|
||||
Error: 2,
|
||||
Info: 3,
|
||||
Authentication: 4,
|
||||
SettingsChange: 5,
|
||||
TagsChange: 6,
|
||||
Success: 'success',
|
||||
Error: 'error',
|
||||
Info: 'info',
|
||||
Authentication: 'auth',
|
||||
SettingsChange: 'settings-change',
|
||||
TagsChange: 'tags-change',
|
||||
|
||||
notify: notify,
|
||||
listen: listen,
|
||||
|
|
|
@ -29,10 +29,16 @@ controllers.push(require('./controllers/home_controller.js'));
|
|||
|
||||
const tags = require('./tags.js');
|
||||
const events = require('./events.js');
|
||||
const views = require('./util/views.js');
|
||||
for (let controller of controllers) {
|
||||
controller.registerRoutes();
|
||||
}
|
||||
|
||||
page.exit((ctx, next) => {
|
||||
views.unlistenToMessages();
|
||||
next();
|
||||
});
|
||||
|
||||
const api = require('./api.js');
|
||||
Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
||||
.then(() => {
|
||||
|
|
|
@ -60,7 +60,9 @@ function getExport() {
|
|||
return _export || {};
|
||||
}
|
||||
|
||||
events.listen(events.TagsChange, refreshExport);
|
||||
events.listen(
|
||||
events.TagsChange,
|
||||
() => { refreshExport(); return true; });
|
||||
|
||||
module.exports = {
|
||||
getExport: getExport,
|
||||
|
|
|
@ -37,31 +37,31 @@ function makeThumbnail(url) {
|
|||
|
||||
function makeRadio(options) {
|
||||
return makeVoidElement(
|
||||
'input',
|
||||
{
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
value: options.value,
|
||||
type: 'radio',
|
||||
checked: options.selectedValue === options.value,
|
||||
required: options.required,
|
||||
}) +
|
||||
_makeLabel(options, {class: 'radio'});
|
||||
'input',
|
||||
{
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
value: options.value,
|
||||
type: 'radio',
|
||||
checked: options.selectedValue === options.value,
|
||||
required: options.required,
|
||||
}) +
|
||||
_makeLabel(options, {class: 'radio'});
|
||||
}
|
||||
|
||||
function makeCheckbox(options) {
|
||||
return makeVoidElement(
|
||||
'input',
|
||||
{
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
value: options.value,
|
||||
type: 'checkbox',
|
||||
checked: options.checked !== undefined ?
|
||||
options.checked : false,
|
||||
required: options.required,
|
||||
}) +
|
||||
_makeLabel(options, {class: 'checkbox'});
|
||||
'input',
|
||||
{
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
value: options.value,
|
||||
type: 'checkbox',
|
||||
checked: options.checked !== undefined ?
|
||||
options.checked : false,
|
||||
required: options.required,
|
||||
}) +
|
||||
_makeLabel(options, {class: 'checkbox'});
|
||||
}
|
||||
|
||||
function makeSelect(options) {
|
||||
|
@ -143,26 +143,6 @@ function makeFlexboxAlign(options) {
|
|||
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
||||
}
|
||||
|
||||
function _messageHandler(target, message, className) {
|
||||
if (!message) {
|
||||
message = 'Unknown message';
|
||||
}
|
||||
const messagesHolder = target.querySelector('.messages');
|
||||
if (!messagesHolder) {
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
/* TODO: animate this */
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = message.replace(/\n/g, '<br/>');
|
||||
node.classList.add('message');
|
||||
node.classList.add(className);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('message-wrapper');
|
||||
wrapper.appendChild(node);
|
||||
messagesHolder.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function _serializeElement(name, attributes) {
|
||||
return [name]
|
||||
.concat(Object.keys(attributes).map(key => {
|
||||
|
@ -186,16 +166,44 @@ function makeVoidElement(name, attributes) {
|
|||
return '<{0}/>'.format(_serializeElement(name, attributes));
|
||||
}
|
||||
|
||||
function listenToMessages(target) {
|
||||
function _messageHandler(target, message, className) {
|
||||
if (!message) {
|
||||
message = 'Unknown message';
|
||||
}
|
||||
const messagesHolder = target.querySelector('.messages');
|
||||
if (!messagesHolder) {
|
||||
return false;
|
||||
}
|
||||
/* TODO: animate this */
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = message.replace(/\n/g, '<br/>');
|
||||
node.classList.add('message');
|
||||
node.classList.add(className);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('message-wrapper');
|
||||
wrapper.appendChild(node);
|
||||
messagesHolder.appendChild(wrapper);
|
||||
return true;
|
||||
}
|
||||
|
||||
function unlistenToMessages() {
|
||||
events.unlisten(events.Success);
|
||||
events.unlisten(events.Error);
|
||||
events.unlisten(events.Info);
|
||||
events.listen(
|
||||
events.Success, msg => { _messageHandler(target, msg, 'success'); });
|
||||
events.listen(
|
||||
events.Error, msg => { _messageHandler(target, msg, 'error'); });
|
||||
events.listen(
|
||||
events.Info, msg => { _messageHandler(target, msg, 'info'); });
|
||||
}
|
||||
|
||||
function listenToMessages(target) {
|
||||
unlistenToMessages();
|
||||
const listen = (eventType, className) => {
|
||||
events.listen(
|
||||
eventType,
|
||||
msg => {
|
||||
return _messageHandler(target, msg, className);
|
||||
});
|
||||
};
|
||||
listen(events.Success, 'success');
|
||||
listen(events.Error, 'error');
|
||||
listen(events.Info, 'info');
|
||||
}
|
||||
|
||||
function clearMessages(target) {
|
||||
|
@ -314,6 +322,7 @@ module.exports = {
|
|||
enableForm: enableForm,
|
||||
disableForm: disableForm,
|
||||
listenToMessages: listenToMessages,
|
||||
unlistenToMessages: unlistenToMessages,
|
||||
clearMessages: clearMessages,
|
||||
decorateValidator: decorateValidator,
|
||||
makeVoidElement: makeVoidElement,
|
||||
|
|
|
@ -10,7 +10,7 @@ class EmptyView {
|
|||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template;
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ class EndlessPageView {
|
|||
const source = this.holderTemplate();
|
||||
const pageHeaderHolder = source.querySelector('.page-header-holder');
|
||||
const pagesHolder = source.querySelector('.pages-holder');
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
this.active = true;
|
||||
this.working = 0;
|
||||
|
|
|
@ -61,7 +61,7 @@ class HelpView {
|
|||
}
|
||||
}
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
|
||||
views.scrollToHash();
|
||||
|
|
|
@ -16,7 +16,7 @@ class HomeView {
|
|||
buildDate: config.meta.buildDate,
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class LoginView {
|
|||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,13 +99,13 @@ class ManualPageView {
|
|||
}));
|
||||
}
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
if (response.total <= (currentPage - 1) * response.pageSize) {
|
||||
events.notify(events.Info, 'No data to show');
|
||||
}
|
||||
}, response => {
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ class PasswordResetView {
|
|||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ class RegistrationView {
|
|||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class SettingsView {
|
|||
});
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ class TagListHeaderView {
|
|||
this._saveButtonClickHandler(e, ctx, target);
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,11 @@ class UserDeleteView {
|
|||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.delete();
|
||||
ctx.delete()
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class UserEditView {
|
|||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ class UserSummaryView {
|
|||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this.template(ctx);
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class UserView {
|
|||
ctx.target = source.querySelector('#user-content-holder');
|
||||
view.render(ctx);
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue