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

View File

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

View File

@ -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 = {};

View File

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

View File

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

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