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: {
|
messageAreaViewPost: {
|
||||||
0: {
|
0: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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