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:
Bryan Ashby 2018-11-12 22:03:28 -07:00
parent 14095d8f03
commit 2b36693240
7 changed files with 290 additions and 29 deletions

View File

@ -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: { messageAreaViewPost: {
0: { 0: {

View File

@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
exports.setCursorStyle = setCursorStyle; exports.setCursorStyle = setCursorStyle;
exports.setEmulatedBaudRate = setEmulatedBaudRate; exports.setEmulatedBaudRate = setEmulatedBaudRate;
exports.vtxHyperlink = vtxHyperlink; exports.vtxHyperlink = vtxHyperlink;
// //
// See also // See also

View File

@ -32,13 +32,14 @@
----/snip/---------------------- ----/snip/----------------------
*/ */
// ENiGMA½ // ENiGMA½
const term = require('./client_term.js'); const term = require('./client_term.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const User = require('./user.js'); const User = require('./user.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const MenuStack = require('./menu_stack.js'); const MenuStack = require('./menu_stack.js');
const ACS = require('./acs.js'); const ACS = require('./acs.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserInterruptQueue = require('./user_interrupt_queue.js');
// deps // deps
const stream = require('stream'); const stream = require('stream');
@ -84,6 +85,7 @@ function Client(/*input, output*/) {
this.menuStack = new MenuStack(this); this.menuStack = new MenuStack(this);
this.acs = new ACS(this); this.acs = new ACS(this);
this.mciCache = {}; this.mciCache = {};
this.interruptQueue = new UserInterruptQueue(this);
this.clearMciCache = function() { this.clearMciCache = function() {
this.mciCache = {}; this.mciCache = {};

View File

@ -15,11 +15,16 @@ exports.getActiveNodeList = getActiveNodeList;
exports.addNewClient = addNewClient; exports.addNewClient = addNewClient;
exports.removeClient = removeClient; exports.removeClient = removeClient;
exports.getConnectionByUserId = getConnectionByUserId; exports.getConnectionByUserId = getConnectionByUserId;
exports.getConnectionByNodeId = getConnectionByNodeId;
const clientConnections = []; 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) { function getActiveNodeList(authUsersOnly) {
@ -29,11 +34,7 @@ function getActiveNodeList(authUsersOnly) {
const now = moment(); const now = moment();
const activeConnections = getActiveConnections().filter(ac => { return _.map(getActiveConnections(authUsersOnly), ac => {
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
});
return _.map(activeConnections, ac => {
const entry = { const entry = {
node : ac.node, node : ac.node,
authenticated : ac.user.isAuthenticated(), authenticated : ac.user.isAuthenticated(),
@ -118,3 +119,7 @@ function removeClient(client) {
function getConnectionByUserId(userId) { function getConnectionByUserId(userId) {
return getActiveConnections().find( ac => userId === ac.user.userId ); return getActiveConnections().find( ac => userId === ac.user.userId );
} }
function getConnectionByNodeId(nodeId) {
return getActiveConnections().find( ac => nodeId == ac.node );
}

View File

@ -25,15 +25,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
this.menuName = options.menuName; this.menuName = options.menuName;
this.menuConfig = options.menuConfig; this.menuConfig = options.menuConfig;
this.client = options.client; this.client = options.client;
//this.menuConfig.options = options.menuConfig.options || {};
this.menuMethods = {}; // methods called from @method's this.menuMethods = {}; // methods called from @method's
this.menuConfig.config = this.menuConfig.config || {}; this.menuConfig.config = this.menuConfig.config || {};
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); 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 = {}; this.viewControllers = {};
// *initial* interruptable state for this menu
this.disableInterruption();
} }
enter() { enter() {
@ -44,6 +42,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
this.detachViewControllers(); this.detachViewControllers();
} }
toggleInterruptionAndDisplayQueued(cb) {
this.enableInterruption();
this.displayQueuedInterruptions( () => {
this.disableInterruption();
return cb(null);
});
}
initSequence() { initSequence() {
const self = this; const self = this;
const mciData = {}; const mciData = {};
@ -51,8 +57,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
async.series( async.series(
[ [
function beforeArtInterrupt(callback) {
return self.toggleInterruptionAndDisplayQueued(callback);
},
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
self.beforeArt(callback); return self.beforeArt(callback);
}, },
function displayMenuArt(callback) { function displayMenuArt(callback) {
if(!_.isString(self.menuConfig.art)) { if(!_.isString(self.menuConfig.art)) {
@ -160,6 +169,48 @@ exports.MenuModule = class MenuModule extends PluginModule {
// nothing in base // 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() { getSaveState() {
// nothing in base // nothing in base
} }
@ -178,11 +229,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
return this.prevMenu(cb); // no next, go to prev 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) { prevMenu(cb) {
return this.client.menuStack.prev(cb); this.displayQueuedInterruptions( () => {
return this.client.menuStack.prev(cb);
});
} }
gotoMenu(name, options, cb) { gotoMenu(name, options, cb) {
@ -234,13 +289,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
autoNextMenu(cb) { autoNextMenu(cb) {
const self = this; const gotoNextMenu = () => {
if(this.haveNext()) {
function gotoNextMenu() { this.displayQueuedInterruptions( () => {
if(self.haveNext()) { return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); });
} else { } else {
return self.prevMenu(cb); return this.prevMenu(cb);
} }
} }

126
core/node_msg.js Normal file
View File

@ -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);
}
}

View File

@ -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);
});
}
};