occaecatidicta
Version:
610 lines (549 loc) • 19.3 kB
text/typescript
import * as countDownLatch from '../../util/countDownLatch';
import * as utils from '../../util/utils';
import { ChannelRemote } from '../remote/frontend/channelRemote';
import { getLogger } from 'omelox-logger';
import { Application } from '../../application';
import { IComponent } from '../../interfaces/IComponent';
import { IStore } from '../../interfaces/IStore';
import { IHandlerFilter } from '../../interfaces/IHandlerFilter';
import { FRONTENDID, UID, SID } from '../../util/constants';
import * as path from 'path';
let logger = getLogger('omelox', path.basename(__filename));
/**
* constant
*/
let ST_INITED = 0;
let ST_DESTROYED = 1;
export interface ChannelServiceOptions {
prefix?: string;
store?: IStore;
broadcastFilter?: IHandlerFilter;
}
/**
* Create and maintain channels for server local.
*
* ChannelService is created by channel component which is a default loaded
* component of omelox and channel service would be accessed by `app.get('channelService')`.
*
* @class
* @constructor
*/
export class ChannelService implements IComponent {
app: Application;
channels: { [key: string]: Channel };
prefix: string;
store: IStore;
broadcastFilter: any;
channelRemote: ChannelRemote;
name: string;
constructor(app: Application, opts ?: ChannelServiceOptions) {
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: (err?: Error) => void) {
restoreChannel(this, cb);
}
/**
* Create channel with name.
*
* @param {String} name channel's name
* @memberOf ChannelService
*/
createChannel(name: string) {
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: string, create ?: boolean) {
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: string) {
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: string, msg: any, uids: { uid: string, sid: string }[], cb?: (err?: Error, result?: void) => void): void;
pushMessageByUids(route: string, msg: any, uids: { uid: string, sid: string }[], opts: any, cb?: (err?: Error, result?: void) => void): void;
pushMessageByUids(route: string, msg: any, uids: { uid: string, sid: string }[], opts?: any, cb?: (err?: Error, result?: void) => void) {
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 message to all the connected clients.
*
* @param {String} stype 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(stype: string, route: string, msg: any, cb?: (err?: Error, result?: void) => void): void;
broadcast(stype: string, route: string, msg: any, opts: any, cb?: (err?: Error, result?: void) => void): void;
broadcast(stype: string, route: string, msg: any, opts?: any, cb?: (err?: Error, result?: void) => void) {
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 ?: string) {
return function (err: Error) {
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: string) {
return (function () {
if (serverId === app.serverId) {
(self.channelRemote as any)[method](route, msg, opts).then(() => genCB(serverId)(null)).catch((err: any) => 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);
}
}
apushMessageByUids: (route: string, msg: any, uids: { uid: string, sid: string }[], opts?: Object) => Promise<void> = utils.promisify(this.pushMessageByUids);
abroadcast: (stype: string, route: string, msg: any, opts?: any) => Promise<void> = utils.promisify(this.broadcast);
}
/**
* 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
*/
export class Channel {
name: string;
groups: { [sid: string]: string[] };
records: { [key: string]: { sid: string, uid: string } };
__channelService__: ChannelService;
state: number;
userAmount: number;
constructor(name: string, service: ChannelService) {
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: string, sid: string) {
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: UID, sid: FRONTENDID) {
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: UID) {
return this.records[uid];
}
/**
* Remove member by uid
* @param uid member to removed
*/
removeMember(uid: 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: string, msg: any, opts ?: any, cb ?: (err: Error | null, result ?: void) => void) {
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);
}
apushMessage: (route: string, msg: any, opts ?: any) => Promise<void> = utils.promisify(this.pushMessage);
}
/**
* 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: UID, sid: FRONTENDID, groups: { [sid: string]: UID[] }) {
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: UID, sid: FRONTENDID, group: UID[]) {
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: ChannelService, route: string, msg: any, groups: { [sid: string]: UID[] }, opts: any, cb: Function) {
let app = channelService.app;
let namespace = 'sys';
let service = 'channelRemote';
let method = 'pushMessage';
let count = Object.keys(groups).length;
let successFlag = false;
let failIds: SID[] = [];
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: string) {
return function (err: Error, fails: SID[]) {
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: FRONTENDID) {
return (function () {
if (sid === app.serverId) {
(channelService.channelRemote as any)[method](route, msg, groups[sid], opts).then((fails: SID[]) => {
rpcCB(sid)(null, fails);
}, (err: Error) => {
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: ChannelService, cb: Function) {
if (!self.store) {
utils.invokeCallback(cb);
return;
} else {
loadAllFromStore(self, genKey(self), function (err: Error, list) {
if (!!err) {
utils.invokeCallback(cb, err);
return;
} else {
if (!list.length || !Array.isArray(list)) {
utils.invokeCallback(cb);
return;
}
let load = function (key: string, name: string) {
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: ChannelService, key: string, value: string) {
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: ChannelService, key: string, value: string) {
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: ChannelService, key: string, cb: (err: Error, list: string[]) => void) {
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: ChannelService, key: string) {
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: ChannelService, name ?: string) {
if (!!name) {
return self.prefix + ':' + self.app.serverId + ':' + name;
} else {
return self.prefix + ':' + self.app.serverId;
}
};
let genValue = function (sid: FRONTENDID, uid: UID) {
return sid + ':' + uid;
};