WIP: User Interrupt Queue
* All queueing of messages/etc. * Queueing across nodes * Start on interruption points for displaying queued items * Start on a multi-node messaging system using such a queue
This commit is contained in:
parent
14095d8f03
commit
2b36693240
|
@ -353,6 +353,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
nodeMessage: {
|
||||
0: {
|
||||
mci: {
|
||||
SM1: {
|
||||
width: 22
|
||||
itemFormat: "|00|07{text} |08(|07{userName}|08)"
|
||||
focusItemFormat: "|00|15{text} |07(|15{userName}|07)"
|
||||
}
|
||||
ET2: {
|
||||
width: 70
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
messageAreaViewPost: {
|
||||
0: {
|
||||
|
|
|
@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
|
|||
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
||||
exports.setCursorStyle = setCursorStyle;
|
||||
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
||||
exports.vtxHyperlink = vtxHyperlink;
|
||||
exports.vtxHyperlink = vtxHyperlink;
|
||||
|
||||
//
|
||||
// See also
|
||||
|
|
|
@ -32,13 +32,14 @@
|
|||
----/snip/----------------------
|
||||
*/
|
||||
// ENiGMA½
|
||||
const term = require('./client_term.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const User = require('./user.js');
|
||||
const Config = require('./config.js').get;
|
||||
const MenuStack = require('./menu_stack.js');
|
||||
const ACS = require('./acs.js');
|
||||
const Events = require('./events.js');
|
||||
const term = require('./client_term.js');
|
||||
const ansi = require('./ansi_term.js');
|
||||
const User = require('./user.js');
|
||||
const Config = require('./config.js').get;
|
||||
const MenuStack = require('./menu_stack.js');
|
||||
const ACS = require('./acs.js');
|
||||
const Events = require('./events.js');
|
||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||
|
||||
// deps
|
||||
const stream = require('stream');
|
||||
|
@ -84,6 +85,7 @@ function Client(/*input, output*/) {
|
|||
this.menuStack = new MenuStack(this);
|
||||
this.acs = new ACS(this);
|
||||
this.mciCache = {};
|
||||
this.interruptQueue = new UserInterruptQueue(this);
|
||||
|
||||
this.clearMciCache = function() {
|
||||
this.mciCache = {};
|
||||
|
|
|
@ -15,11 +15,16 @@ exports.getActiveNodeList = getActiveNodeList;
|
|||
exports.addNewClient = addNewClient;
|
||||
exports.removeClient = removeClient;
|
||||
exports.getConnectionByUserId = getConnectionByUserId;
|
||||
exports.getConnectionByNodeId = getConnectionByNodeId;
|
||||
|
||||
const clientConnections = [];
|
||||
exports.clientConnections = clientConnections;
|
||||
exports.clientConnections = clientConnections;
|
||||
|
||||
function getActiveConnections() { return clientConnections; }
|
||||
function getActiveConnections(authUsersOnly = false) {
|
||||
return clientConnections.filter(conn => {
|
||||
return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly);
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveNodeList(authUsersOnly) {
|
||||
|
||||
|
@ -29,11 +34,7 @@ function getActiveNodeList(authUsersOnly) {
|
|||
|
||||
const now = moment();
|
||||
|
||||
const activeConnections = getActiveConnections().filter(ac => {
|
||||
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
|
||||
});
|
||||
|
||||
return _.map(activeConnections, ac => {
|
||||
return _.map(getActiveConnections(authUsersOnly), ac => {
|
||||
const entry = {
|
||||
node : ac.node,
|
||||
authenticated : ac.user.isAuthenticated(),
|
||||
|
@ -118,3 +119,7 @@ function removeClient(client) {
|
|||
function getConnectionByUserId(userId) {
|
||||
return getActiveConnections().find( ac => userId === ac.user.userId );
|
||||
}
|
||||
|
||||
function getConnectionByNodeId(nodeId) {
|
||||
return getActiveConnections().find( ac => nodeId == ac.node );
|
||||
}
|
||||
|
|
|
@ -25,15 +25,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
this.menuName = options.menuName;
|
||||
this.menuConfig = options.menuConfig;
|
||||
this.client = options.client;
|
||||
//this.menuConfig.options = options.menuConfig.options || {};
|
||||
this.menuMethods = {}; // methods called from @method's
|
||||
this.menuConfig.config = this.menuConfig.config || {};
|
||||
|
||||
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls);
|
||||
|
||||
//this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls;
|
||||
|
||||
this.viewControllers = {};
|
||||
|
||||
// *initial* interruptable state for this menu
|
||||
this.disableInterruption();
|
||||
}
|
||||
|
||||
enter() {
|
||||
|
@ -44,6 +42,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
this.detachViewControllers();
|
||||
}
|
||||
|
||||
toggleInterruptionAndDisplayQueued(cb) {
|
||||
this.enableInterruption();
|
||||
this.displayQueuedInterruptions( () => {
|
||||
this.disableInterruption();
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
initSequence() {
|
||||
const self = this;
|
||||
const mciData = {};
|
||||
|
@ -51,8 +57,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
|
||||
async.series(
|
||||
[
|
||||
function beforeArtInterrupt(callback) {
|
||||
return self.toggleInterruptionAndDisplayQueued(callback);
|
||||
},
|
||||
function beforeDisplayArt(callback) {
|
||||
self.beforeArt(callback);
|
||||
return self.beforeArt(callback);
|
||||
},
|
||||
function displayMenuArt(callback) {
|
||||
if(!_.isString(self.menuConfig.art)) {
|
||||
|
@ -160,6 +169,48 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
// nothing in base
|
||||
}
|
||||
|
||||
neverInterruptable() {
|
||||
return this.menuConfig.config.interruptable === 'never';
|
||||
}
|
||||
|
||||
enableInterruption() {
|
||||
if(!this.neverInterruptable()) {
|
||||
this.interruptable = true;
|
||||
}
|
||||
}
|
||||
|
||||
disableInterruption() {
|
||||
if(!this.neverInterruptable()) {
|
||||
this.interruptable = false;
|
||||
}
|
||||
}
|
||||
|
||||
displayQueuedInterruptions(cb) {
|
||||
if(true !== this.interruptable) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
async.whilst(
|
||||
() => this.client.interruptQueue.hasItems(),
|
||||
next => {
|
||||
this.client.interruptQueue.display( (err, interruptItem) => {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(interruptItem.pause) {
|
||||
return this.pausePrompt(next);
|
||||
}
|
||||
|
||||
return next(null);
|
||||
});
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getSaveState() {
|
||||
// nothing in base
|
||||
}
|
||||
|
@ -178,11 +229,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
return this.prevMenu(cb); // no next, go to prev
|
||||
}
|
||||
|
||||
return this.client.menuStack.next(cb);
|
||||
this.displayQueuedInterruptions( () => {
|
||||
return this.client.menuStack.next(cb);
|
||||
});
|
||||
}
|
||||
|
||||
prevMenu(cb) {
|
||||
return this.client.menuStack.prev(cb);
|
||||
this.displayQueuedInterruptions( () => {
|
||||
return this.client.menuStack.prev(cb);
|
||||
});
|
||||
}
|
||||
|
||||
gotoMenu(name, options, cb) {
|
||||
|
@ -234,13 +289,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
}
|
||||
|
||||
autoNextMenu(cb) {
|
||||
const self = this;
|
||||
|
||||
function gotoNextMenu() {
|
||||
if(self.haveNext()) {
|
||||
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
|
||||
const gotoNextMenu = () => {
|
||||
if(this.haveNext()) {
|
||||
this.displayQueuedInterruptions( () => {
|
||||
return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
|
||||
});
|
||||
} else {
|
||||
return self.prevMenu(cb);
|
||||
return this.prevMenu(cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const { MenuModule } = require('./menu_module.js');
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const {
|
||||
getActiveNodeList,
|
||||
getConnectionByNodeId,
|
||||
} = require('./client_connections.js');
|
||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||
|
||||
// deps
|
||||
const series = require('async/series');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Node Message',
|
||||
desc : 'Multi-node messaging',
|
||||
author : 'NuSkooler',
|
||||
};
|
||||
|
||||
const FormIds = {
|
||||
sendMessage : 0,
|
||||
};
|
||||
|
||||
const MciViewIds = {
|
||||
sendMessage : {
|
||||
nodeSelect : 1,
|
||||
message : 2,
|
||||
preview : 3,
|
||||
|
||||
customRangeStart : 10,
|
||||
}
|
||||
}
|
||||
|
||||
exports.getModule = class NodeMessageModule extends MenuModule {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs });
|
||||
|
||||
this.menuMethods = {
|
||||
sendMessage : (formData, extraArgs, cb) => {
|
||||
const nodeId = formData.value.node;
|
||||
const message = formData.value.message;
|
||||
|
||||
const interruptItem = {
|
||||
contents : message,
|
||||
}
|
||||
|
||||
if(0 === nodeId) {
|
||||
// ALL nodes
|
||||
UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client);
|
||||
} else {
|
||||
UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]);
|
||||
}
|
||||
|
||||
return this.prevMenu(cb);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mciReady(mciData, cb) {
|
||||
super.mciReady(mciData, err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
series(
|
||||
[
|
||||
(next) => {
|
||||
return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, next);
|
||||
},
|
||||
(next) => {
|
||||
const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect);
|
||||
if(!nodeSelectView) {
|
||||
return next(Errors.MissingMci(`Missing node selection MCI ${MciViewIds.sendMessage.nodeSelect}`));
|
||||
}
|
||||
|
||||
this.prepareNodeList();
|
||||
|
||||
nodeSelectView.on('index update', idx => {
|
||||
this.nodeListSelectionIndexUpdate(idx);
|
||||
});
|
||||
|
||||
nodeSelectView.setItems(this.nodeList);
|
||||
nodeSelectView.redraw();
|
||||
this.nodeListSelectionIndexUpdate(0);
|
||||
return next(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
prepareNodeList() {
|
||||
// standard node list with {text} field added for compliance
|
||||
this.nodeList = [{
|
||||
text : '-ALL-',
|
||||
// dummy fields:
|
||||
node : 0,
|
||||
authenticated : false,
|
||||
userId : 0,
|
||||
action : 'N/A',
|
||||
userName : 'Everyone',
|
||||
realName : 'All Users',
|
||||
location : 'N/A',
|
||||
affils : 'N/A',
|
||||
timeOn : 'N/A',
|
||||
}].concat(getActiveNodeList(true)
|
||||
.map(node => Object.assign(node, { text : node.node.toString() } ))
|
||||
).filter(node => node.node !== this.client.node); // remove our client's node
|
||||
this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node
|
||||
}
|
||||
|
||||
nodeListSelectionIndexUpdate(idx) {
|
||||
const node = this.nodeList[idx];
|
||||
if(!node) {
|
||||
return;
|
||||
}
|
||||
this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Art = require('./art.js');
|
||||
const {
|
||||
getActiveConnections
|
||||
} = require('./client_connections.js');
|
||||
const ANSI = require('./ansi_term.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = class UserInterruptQueue
|
||||
{
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
static queueGlobal(interruptItem, connections) {
|
||||
connections.forEach(conn => {
|
||||
conn.interruptQueue.queueItem(interruptItem);
|
||||
});
|
||||
}
|
||||
|
||||
// common shortcut: queue global, all active clients minus |client|
|
||||
static queueGlobalOtherActive(interruptItem, client) {
|
||||
const otherConnections = getActiveConnections(true).filter(ac => ac.node !== client.node);
|
||||
return UserInterruptQueue.queueGlobal(interruptItem, otherConnections );
|
||||
}
|
||||
|
||||
queueItem(interruptItem) {
|
||||
interruptItem.pause = _.get(interruptItem, 'pause', true);
|
||||
this.queue.push(interruptItem);
|
||||
}
|
||||
|
||||
hasItems() {
|
||||
return this.queue.length > 0;
|
||||
}
|
||||
|
||||
display(cb) {
|
||||
const interruptItem = this.queue.pop();
|
||||
if(!interruptItem) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
if(interruptItem.cls) {
|
||||
this.client.term.rawWrite(ANSI.clearScreen());
|
||||
} else {
|
||||
this.client.term.rawWrite('\r\n\r\n');
|
||||
}
|
||||
|
||||
Art.display(this.client, interruptItem.contents, err => {
|
||||
return cb(err, interruptItem);
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue