UNPKG

pomelo-upgrade

Version:
664 lines (607 loc) 16.7 kB
'use strict'; const countDownLatch = require('../../util/countDownLatch'); const utils = require('../../util/utils'); const ChannelRemote = require('../remote/frontend/channelRemote'); const logger = require('pomelo-logger-upgrade').getLogger('pomelo', __filename); /** * constant */ const ST_INITED = 0; const ST_DESTROYED = 1; /** * Create and maintain channels for server local. * * ChannelService is created by channel component which is a default loaded * component of pomelo and channel service would be accessed by `app.get('channelService')`. * * @class * @constructor */ class ChannelService { constructor(app, opts) { opts = opts || {}; this.app = app; this.channels = {}; this.prefix = opts.prefix; this.store = opts.store; this.broadcastFilter = opts.broadcastFilter; this.channelRemote = new ChannelRemote(app); } start(cb) { restoreChannel(this, cb); if (!this.store) { utils.invokeCallback(cb); return; } loadAllFromStore(this, genKey(this), (err, list) => { if (err) { utils.invokeCallback(cb, err); return; } if (!list.length || !Array.isArray(list)) { utils.invokeCallback(cb); return; } const load = (key, loadName) => { return (function() { loadAllFromStore(this, key, (err, items) => { if (err) return; for (let j = 0; j < items.length; j++) { const array = items[j].split(':'); const sid = array[0]; const uid = array[1]; const channel = this.channels[loadName]; const res = add(uid, sid, channel.groups); if (res) { channel.records[uid] = {sid : sid, uid : uid}; } } }); })(); }; for (let i = 0; i < list.length; i++) { const name = list[i].slice(genKey(self).length + 1); self.channels[name] = new Channel(name, self); load(list[i], name); } utils.invokeCallback(cb); }); } /** * Create channel with name. * * @param {string} name channel's name * @memberOf ChannelService */ createChannel(name) { if (this.channels[name]) { return this.channels[name]; } const 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 && Boolean(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)); } /** * Push message by uids. * Group the uids by group. ignore any uid if sid not specified. * * @param {string} route message route * @param {object} msg message that would be sent to client * @param {Array} uids the receiver info list, [{uid: userId, sid: frontendServerId}] * @param {object} opts user-defined push options, optional * @param {function} cb cb(err) * @memberOf ChannelService */ 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; } const groups = {}; let 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 message to all the connected clients. * * @param {string} frontendServerType frontend server type string * @param {string} route route string * @param {object} msg message * @param {object} opts user-defined broadcast options, optional * opts.binded: push to binded sessions or all the sessions * opts.filterParam: parameters for broadcast filter. * @param {function} cb callback * @memberOf ChannelService */ broadcast(frontendServerType, route, msg, opts, cb) { const app = this.app; const namespace = 'sys'; const service = 'channelRemote'; const method = 'broadcast'; const servers = app.getServersByType(frontendServerType); if (!servers || servers.length === 0) { // server list is empty utils.invokeCallback(cb); return; } const count = servers.length; let successFlag = false; const latch = countDownLatch.createCountDownLatch(count, function() { if (!successFlag) { utils.invokeCallback(cb, new Error('broadcast fails')); return; } utils.invokeCallback(cb, null); }); const genCB = (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(); }; }; const self = this; const sendMessage = (serverId) => { return (function() { if (serverId === app.serverId) { self.channelRemote[method](route, msg, opts, genCB()); } else { app.rpcInvoke(serverId, {namespace : namespace, service : service, method : method, args : [route, msg, opts]}, genCB(serverId)); } }()); }; opts = { type : 'broadcast', userOptions : opts || {} }; // for compatiblity opts.isBroadcast = true; if (opts.userOptions) { opts.binded = opts.userOptions.binded; opts.filterParam = opts.userOptions.filterParam; } for (let i = 0, l = count; i < l; i++) { sendMessage(servers[i].id); } } } /** * 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.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; } const 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; } const res = deleteFrom(uid, sid, this.groups[sid]); if (res) { delete this.records[uid]; this.userAmount = this.userAmount - 1; } if (this.userAmount < 0) this.userAmount = 0;// robust removeFromStore(this.__channelService__, genKey(this.__channelService__, this.name), genValue(sid, uid)); 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() { const res = []; const groups = this.groups; let group, i, l; for (const 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]; } /** * 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(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); } } module.exports = function(app, opts) { return new ChannelService(app, opts); }; /** * 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] */ const add = (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 */ const deleteFrom = (uid, sid, group) => { if (!uid || !sid || !group) { return false; } const index = group.indexOf(uid); if (index >= 0) { group.splice(index, 1); return true; } return false; }; /** * push message by group * * @param {string} route route route message * @param {object} msg message that would be sent to client * @param {object} groups grouped uids, , key: sid, value: [uid] * @param {object} opts push options * @param {function} cb cb(err) * * @api private */ const sendMessageByGroup = (channelService, route, msg, groups, opts, cb) => { const app = channelService.app; const namespace = 'sys'; const service = 'channelRemote'; const method = 'pushMessage'; const count = utils.size(groups); 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; } const latch = countDownLatch.createCountDownLatch(count, function() { if (!successFlag) { utils.invokeCallback(cb, new Error('all uids push message fail')); return; } utils.invokeCallback(cb, null, failIds); }); const rpcCB = (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; const sendMessage = (sid) => { return (function() { if (sid === app.serverId) { channelService.channelRemote[method](route, msg, groups[sid], opts, rpcCB(sid)); } else { app.rpcInvoke(sid, { namespace : namespace, service : service, method : method, args : [route, msg, groups[sid], opts]}, rpcCB(sid)); } })(); }; let group; for (const sid in groups) { group = groups[sid]; if (group && group.length > 0) { sendMessage(sid); } else { // empty group process.nextTick(rpcCB(sid)); } } }; const restoreChannel = (self, cb) => { }; const addToStore = (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.stack); } }); } }; const removeFromStore = (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.stack); } }); } }; const loadAllFromStore = (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.stack); utils.invokeCallback(cb, err); } else { utils.invokeCallback(cb, null, list); } }); } }; const removeAllFromStore = (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.stack); } }); } }; const genKey = (self, name) => { if (name) { return `${self.prefix}:${self.app.serverId}:${name}`; } return `${self.prefix}:${self.app.serverId}`; }; const genValue = (sid, uid) => { return `${sid}:${uid}`; };