pinusmod
Version:
[](https://travis-ci.org/node-pinus/pinus)
514 lines (513 loc) • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Channel = exports.ChannelService = void 0;
const countDownLatch = require("../../util/countDownLatch");
const utils = require("../../util/utils");
const channelRemote_1 = require("../remote/frontend/channelRemote");
const pinusmod_logger_1 = require("pinusmod-logger");
const path = require("path");
let logger = (0, pinusmod_logger_1.getLogger)('pinus', path.basename(__filename));
/**
* constant
*/
let ST_INITED = 0;
let ST_DESTROYED = 1;
/**
* Create and maintain channels for server local.
*
* ChannelService is created by channel component which is a default loaded
* component of pinus and channel service would be accessed by `app.get('channelService')`.
*
* @class
* @constructor
*/
class ChannelService {
constructor(app, opts) {
this.apushMessageByUids = utils.promisify(this.pushMessageByUids);
this.abroadcast = utils.promisify(this.broadcast);
opts = opts || {};
this.app = app;
this.channels = {};
this.prefix = opts.prefix;
this.store = opts.store;
this.broadcastFilter = opts.broadcastFilter;
this.channelRemote = new channelRemote_1.ChannelRemote(app);
}
start(cb) {
restoreChannel(this, cb);
}
/**
* Create channel with name.
*
* @param {String} name channel's name
* @memberOf ChannelService
*/
createChannel(name) {
if (this.channels[name]) {
return this.channels[name];
}
let c = new Channel(name, this);
addToStore(this, genKey(this), genKey(this, name));
this.channels[name] = c;
return c;
}
/**
* Get channel by name.
*
* @param {String} name channel's name
* @param {Boolean} create if true, create channel
* @return {Channel}
* @memberOf ChannelService
*/
getChannel(name, create) {
let channel = this.channels[name];
if (!channel && !!create) {
channel = this.channels[name] = new Channel(name, this);
addToStore(this, genKey(this), genKey(this, name));
}
return channel;
}
/**
* Destroy channel by name.
*
* @param {String} name channel name
* @memberOf ChannelService
*/
destroyChannel(name) {
delete this.channels[name];
removeFromStore(this, genKey(this), genKey(this, name));
removeAllFromStore(this, genKey(this, name));
}
pushMessageByUids(route, msg, uids, opts, cb) {
if (typeof route !== 'string') {
cb = opts;
opts = uids;
uids = msg;
msg = route;
route = msg.route;
}
if (!cb && typeof opts === 'function') {
cb = opts;
opts = {};
}
if (!uids || uids.length === 0) {
utils.invokeCallback(cb, new Error('uids should not be empty'));
return;
}
let groups = {}, record;
for (let i = 0, l = uids.length; i < l; i++) {
record = uids[i];
add(record.uid, record.sid, groups);
}
sendMessageByGroup(this, route, msg, groups, opts, cb);
}
broadcast(stype, route, msg, opts, cb) {
let app = this.app;
let namespace = 'sys';
let service = 'channelRemote';
let method = 'broadcast';
let servers = app.getServersByType(stype);
if (!servers || servers.length === 0) {
// server list is empty
utils.invokeCallback(cb);
return;
}
if (!cb && typeof opts === 'function') {
cb = opts;
opts = undefined;
}
let count = servers.length;
let successFlag = false;
let latch = countDownLatch.createCountDownLatch(count, function () {
if (!successFlag) {
utils.invokeCallback(cb, new Error('broadcast fails'));
return;
}
utils.invokeCallback(cb, null);
});
let genCB = function (serverId) {
return function (err) {
if (err) {
logger.error('[broadcast] fail to push message to serverId: ' + serverId + ', err:' + err.stack);
latch.done();
return;
}
successFlag = true;
latch.done();
};
};
opts = { type: 'broadcast', userOptions: opts || {} };
// for compatiblity
opts.isBroadcast = true;
if (opts.userOptions) {
opts.binded = opts.userOptions.binded;
opts.filterParam = opts.userOptions.filterParam;
}
let self = this;
let sendMessage = function (serverId) {
return (function () {
if (serverId === app.serverId) {
self.channelRemote[method](route, msg, opts).then(() => genCB(serverId)(null)).catch((err) => genCB(serverId)(err));
}
else {
app.rpcInvoke(serverId, {
namespace: namespace, service: service,
method: method, args: [route, msg, opts]
}, genCB(serverId));
}
}());
};
for (let i = 0, l = count; i < l; i++) {
sendMessage(servers[i].id);
}
}
}
exports.ChannelService = ChannelService;
/**
* Channel maintains the receiver collection for a subject. You can
* add users into a channel and then broadcast message to them by channel.
*
* @class channel
* @constructor
*/
class Channel {
constructor(name, service) {
this.apushMessage = utils.promisify(this.pushMessage);
this.name = name;
this.groups = {}; // group map for uids. key: sid, value: [uid]
this.records = {}; // member records. key: uid
this.__channelService__ = service;
this.state = ST_INITED;
this.userAmount = 0;
}
/**
* Add user to channel.
*
* @param {Number} uid user id
* @param {String} sid frontend server id which user has connected to
*/
add(uid, sid) {
if (this.state > ST_INITED) {
return false;
}
else {
let res = add(uid, sid, this.groups);
if (res) {
this.records[uid] = { sid: sid, uid: uid };
this.userAmount = this.userAmount + 1;
addToStore(this.__channelService__, genKey(this.__channelService__, this.name), genValue(sid, uid));
}
return res;
}
}
/**
* Remove user from channel.
*
* @param {Number} uid user id
* @param {String} sid frontend server id which user has connected to.
* @return [Boolean] true if success or false if fail
*/
leave(uid, sid) {
if (!uid || !sid) {
return false;
}
let res = deleteFrom(uid, sid, this.groups[sid]);
if (res) {
delete this.records[uid];
this.userAmount = this.userAmount - 1;
removeFromStore(this.__channelService__, genKey(this.__channelService__, this.name), genValue(sid, uid));
}
if (this.userAmount < 0)
this.userAmount = 0; // robust
if (this.groups[sid] && this.groups[sid].length === 0) {
delete this.groups[sid];
}
return res;
}
/**
* Get channel UserAmount in a channel.
*
* @return {number } channel member amount
*/
getUserAmount() {
return this.userAmount;
}
/**
* Get channel members.
*
* <b>Notice:</b> Heavy operation.
*
* @return {Array} channel member uid list
*/
getMembers() {
let res = [], groups = this.groups;
let group, i, l;
for (let sid in groups) {
group = groups[sid];
for (i = 0, l = group.length; i < l; i++) {
res.push(group[i]);
}
}
return res;
}
/**
* Get Member info.
*
* @param {String} uid user id
* @return {Object} member info
*/
getMember(uid) {
return this.records[uid];
}
/**
* Remove member by uid
* @param uid member to removed
*/
removeMember(uid) {
let member = this.getMember(uid);
if (member)
return this.leave(member.uid, member.sid);
else
return false;
}
/**
* Destroy channel.
*/
destroy() {
this.state = ST_DESTROYED;
this.__channelService__.destroyChannel(this.name);
}
/**
* Push message to all the members in the channel
*
* @param {String} route message route
* @param {Object} msg message that would be sent to client
* @param {Object} opts user-defined push options, optional
* @param {Function} cb callback function
*/
pushMessage(route, msg, opts, cb) {
if (this.state !== ST_INITED) {
utils.invokeCallback(cb, new Error('channel is not running now'));
return;
}
if (typeof route !== 'string') {
cb = opts;
opts = msg;
msg = route;
route = msg.route;
}
if (!cb && typeof opts === 'function') {
cb = opts;
opts = {};
}
sendMessageByGroup(this.__channelService__, route, msg, this.groups, opts, cb);
}
}
exports.Channel = Channel;
/**
* add uid and sid into group. ignore any uid that uid not specified.
*
* @param uid user id
* @param sid server id
* @param groups {Object} grouped uids, , key: sid, value: [uid]
*/
let add = function (uid, sid, groups) {
if (!sid) {
logger.warn('ignore uid %j for sid not specified.', uid);
return false;
}
let group = groups[sid];
if (!group) {
group = [];
groups[sid] = group;
}
group.push(uid);
return true;
};
/**
* delete element from array
*/
let deleteFrom = function (uid, sid, group) {
if (!uid || !sid || !group) {
return false;
}
for (let i = 0, l = group.length; i < l; i++) {
if (group[i] === uid) {
group.splice(i, 1);
return true;
}
}
return false;
};
/**
* push message by group
*
* @param route {String} route route message
* @param msg {Object} message that would be sent to client
* @param groups {Object} grouped uids, , key: sid, value: [uid]
* @param opts {Object} push options
* @param cb {Function} cb(err)
*
* @api private
*/
let sendMessageByGroup = function (channelService, route, msg, groups, opts, cb) {
let app = channelService.app;
let namespace = 'sys';
let service = 'channelRemote';
let method = 'pushMessage';
let count = Object.keys(groups).length;
let successFlag = false;
let failIds = [];
logger.debug('[%s] channelService sendMessageByGroup route: %s, msg: %j, groups: %j, opts: %j', app.serverId, route, msg, groups, opts);
if (count === 0) {
// group is empty
utils.invokeCallback(cb);
return;
}
let latch = countDownLatch.createCountDownLatch(count, function () {
if (!successFlag) {
utils.invokeCallback(cb, new Error('all uids push message fail'));
return;
}
utils.invokeCallback(cb, null, failIds);
});
let rpcCB = function (serverId) {
return function (err, fails) {
if (err) {
logger.error('[pushMessage] fail to dispatch msg to serverId: ' + serverId + ', err:' + err.stack);
latch.done();
return;
}
if (fails) {
failIds = failIds.concat(fails);
}
successFlag = true;
latch.done();
};
};
opts = { type: 'push', userOptions: opts || {} };
// for compatiblity
opts.isPush = true;
let sendMessage = function (sid) {
return (function () {
if (sid === app.serverId) {
channelService.channelRemote[method](route, msg, groups[sid], opts).then((fails) => {
rpcCB(sid)(null, fails);
}, (err) => {
rpcCB(sid)(err, null);
});
}
else {
app.rpcInvoke(sid, {
namespace: namespace, service: service,
method: method, args: [route, msg, groups[sid], opts]
}, rpcCB(sid));
}
})();
};
let group;
for (let sid in groups) {
group = groups[sid];
if (group && group.length > 0) {
sendMessage(sid);
}
else {
// empty group
process.nextTick(rpcCB(sid));
}
}
};
let restoreChannel = function (self, cb) {
if (!self.store) {
utils.invokeCallback(cb);
return;
}
else {
loadAllFromStore(self, genKey(self), function (err, list) {
if (!!err) {
utils.invokeCallback(cb, err);
return;
}
else {
if (!list.length || !Array.isArray(list)) {
utils.invokeCallback(cb);
return;
}
let load = function (key, name) {
return (function () {
let channelName = name;
loadAllFromStore(self, key, function (err, items) {
for (let j = 0; j < items.length; j++) {
let array = items[j].split(':');
let sid = array[0];
let uid = array[1];
let channel = self.channels[channelName];
let res = add(uid, sid, channel.groups);
if (res) {
channel.records[uid] = { sid: sid, uid: uid };
}
}
});
})();
};
for (let i = 0; i < list.length; i++) {
let name = list[i].slice(genKey(self).length + 1);
self.channels[name] = new Channel(name, self);
load(list[i], name);
}
utils.invokeCallback(cb);
}
});
}
};
let addToStore = function (self, key, value) {
if (!!self.store) {
self.store.add(key, value, function (err) {
if (!!err) {
logger.error('add key: %s value: %s to store, with err: %j', key, value, err);
}
});
}
};
let removeFromStore = function (self, key, value) {
if (!!self.store) {
self.store.remove(key, value, function (err) {
if (!!err) {
logger.error('remove key: %s value: %s from store, with err: %j', key, value, err);
}
});
}
};
let loadAllFromStore = function (self, key, cb) {
if (!!self.store) {
self.store.load(key, function (err, list) {
if (!!err) {
logger.error('load key: %s from store, with err: %j', key, err);
utils.invokeCallback(cb, err);
}
else {
utils.invokeCallback(cb, null, list);
}
});
}
};
let removeAllFromStore = function (self, key) {
if (!!self.store) {
self.store.removeAll(key, function (err) {
if (!!err) {
logger.error('remove key: %s all members from store, with err: %j', key, err);
}
});
}
};
let genKey = function (self, name) {
if (!!name) {
return self.prefix + ':' + self.app.serverId + ':' + name;
}
else {
return self.prefix + ':' + self.app.serverId;
}
};
let genValue = function (sid, uid) {
return sid + ':' + uid;
};