/** @module leancloud-realtime */
import d from 'debug';
import uuid from 'uuid/v4';
import IMClient from './im-client';
import { RECONNECT, RECONNECT_ERROR } from './events/core';
import { Conversation } from './conversations';
import { MessageQueryDirection } from './conversations/conversation-base';
import Message, { MessageStatus } from './messages/message';
import BinaryMessage from './messages/binary-message';
import TextMessage from './messages/text-message';
import TypedMessage from './messages/typed-message';
import RecalledMessage from './messages/recalled-message';
import MessageParser from './message-parser';
import { trim, internal, finalize } from './utils';
const debug = d('LC:IMPlugin');
/**
* 消息优先级枚举
* @enum {Number}
* @since 3.3.0
*/
const MessagePriority = {
/** 高 */
HIGH: 1,
/** 普通 */
NORMAL: 2,
/** 低 */
LOW: 3,
};
Object.freeze(MessagePriority);
/**
* 为 Conversation 定义一个新属性
* @param {String} prop 属性名
* @param {Object} [descriptor] 属性的描述符,参见 {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#Description getOwnPropertyDescriptor#Description - MDN},默认为该属性名对应的 Conversation 自定义属性的 getter/setter
* @returns void
* @example
*
* conversation.get('type');
* conversation.set('type', 1);
*
* // equals to
* defineConversationProperty('type');
* conversation.type;
* conversation.type = 1;
*/
const defineConversationProperty = (
prop,
descriptor = {
get() {
return this.get(prop);
},
set(value) {
this.set(prop, value);
},
}
) => {
Object.defineProperty(Conversation.prototype, prop, descriptor);
};
export {
/**
* @see Message
*/
Message,
/**
* @see BinaryMessage
*/
BinaryMessage,
/**
* @see TypedMessage
*/
TypedMessage,
/**
* @see TextMessage
*/
TextMessage,
/**
* @see RecalledMessage
*/
RecalledMessage,
MessagePriority,
MessageStatus,
MessageQueryDirection,
defineConversationProperty,
};
export {
/**
* decorator,定义消息类的类型常量
* @function
* @param {Number} type 自定义类型请使用正整数
* @example @messageType(1)
* class CustomMessage extends TypedMessage {}
*
* // 不支持 decorator 的情况下可以这样使用
* class CustomMessage extends TypedMessage {
* //...
* }
* messageType(1)(CustomMessage);
*/
messageType,
/**
* decorator,定义消息类的自定义字段
* @function
* @param {String[]} fields 自定义字段
* @example @messageField(['foo'])
* class CustomMessage extends TypedMessage {
* constructor(foo) {
* super();
* this.foo = foo;
* }
* }
*
* // 不支持 decorator 的情况下可以这样使用
* class CustomMessage extends TypedMessage {
* constructor(foo) {
* super();
* this.foo = foo;
* }
* //...
* }
* messageField(['foo'])(CustomMessage);
*/
messageField,
IE10Compatible,
} from './messages/helpers';
export { ConversationMemberRole } from './conversation-member-info';
export {
/**
* @see Conversation
*/
Conversation,
/**
* @see ChatRoom
*/
ChatRoom,
/**
* @see ServiceConversation
*/
ServiceConversation,
/**
* @see TemporaryConversation
*/
TemporaryConversation,
} from './conversations';
const onRealtimeCreate = realtime => {
/* eslint-disable no-param-reassign */
const deviceId = uuid();
realtime._IMClients = {};
realtime._IMClientsCreationCount = 0;
const messageParser = new MessageParser(realtime._plugins);
realtime._messageParser = messageParser;
const signAVUser = async user =>
realtime._request({
method: 'POST',
path: '/rtm/sign',
data: {
session_token: user.getSessionToken(),
},
});
/**
* 注册消息类
*
* 在接收消息、查询消息时,会按照消息类注册顺序的逆序依次尝试解析消息内容
*
* @memberof Realtime
* @instance
* @param {Function | Function[]} messageClass 消息类,需要实现 {@link AVMessage} 接口,
* 建议继承自 {@link TypedMessage}
* @throws {TypeError} 如果 messageClass 没有实现 {@link AVMessage} 接口则抛出异常
*/
const register = messageParser.register.bind(messageParser);
/**
* 创建一个即时通讯客户端,多次创建相同 id 的客户端会返回同一个实例
* @memberof Realtime
* @instance
* @param {String|AV.User} [identity] 客户端 identity,如果不指定该参数,服务端会随机生成一个字符串作为 identity,
* 如果传入一个已登录的 AV.User,则会使用该用户的 id 作为客户端 identity 登录。
* @param {Object} [options]
* @param {Function} [options.signatureFactory] open session 时的签名方法 // TODO need details
* @param {Function} [options.conversationSignatureFactory] 对话创建、增减成员操作时的签名方法
* @param {Function} [options.blacklistSignatureFactory] 黑名单操作时的签名方法
* @param {String} [options.tag] 客户端类型标记,以支持单点登录功能
* @param {String} [options.isReconnect=false] 单点登录时标记该次登录是不是应用启动时自动重新登录
* @return {Promise.<IMClient>}
*/
const createIMClient = async (
identity,
{ tag, isReconnect, ...clientOptions } = {},
lagecyTag
) => {
let id;
const buildinOptions = {};
if (identity) {
if (typeof identity === 'string') {
id = identity;
} else if (identity.id && identity.getSessionToken) {
({ id } = identity);
const sessionToken = identity.getSessionToken();
if (!sessionToken) {
throw new Error('User must be authenticated');
}
buildinOptions.signatureFactory = signAVUser;
} else {
throw new TypeError('Identity must be a String or an AV.User');
}
if (realtime._IMClients[id] !== undefined) {
return realtime._IMClients[id];
}
}
if (lagecyTag) {
console.warn(
'DEPRECATION createIMClient tag param: Use options.tag instead.'
);
}
const _tag = tag || lagecyTag;
const promise = realtime
._open()
.then(connection => {
const client = new IMClient(
id,
{ ...buildinOptions, ...clientOptions },
{
_connection: connection,
_request: realtime._request.bind(realtime),
_messageParser: messageParser,
_plugins: realtime._plugins,
_identity: identity,
}
);
connection.on(RECONNECT, () =>
client
._open(realtime._options.appId, _tag, deviceId, true)
/**
* 客户端连接恢复正常,该事件通常在 {@link Realtime#event:RECONNECT} 之后发生
* @event IMClient#RECONNECT
* @see Realtime#event:RECONNECT
* @since 3.2.0
*/
/**
* 客户端重新登录发生错误(网络连接已恢复,但重新登录错误)
* @event IMClient#RECONNECT_ERROR
* @since 3.2.0
*/
.then(
() => client.emit(RECONNECT),
error => client.emit(RECONNECT_ERROR, error)
)
);
internal(client)._eventemitter.on(
'beforeclose',
() => {
delete realtime._IMClients[client.id];
if (realtime._firstIMClient === client) {
delete realtime._firstIMClient;
}
},
realtime
);
internal(client)._eventemitter.on(
'close',
() => {
realtime._deregister(client);
},
realtime
);
return client
._open(realtime._options.appId, _tag, deviceId, isReconnect)
.then(() => {
realtime._IMClients[client.id] = client;
realtime._IMClientsCreationCount += 1;
if (realtime._IMClientsCreationCount === 1) {
client._omitPeerId(true);
realtime._firstIMClient = client;
} else if (
realtime._IMClientsCreationCount > 1 &&
realtime._firstIMClient
) {
realtime._firstIMClient._omitPeerId(false);
}
realtime._register(client);
return client;
})
.catch(error => {
delete realtime._IMClients[client.id];
throw error;
});
})
.then(
...finalize(() => {
realtime._deregisterPending(promise);
})
)
.catch(error => {
delete realtime._IMClients[id];
throw error;
});
if (identity) {
realtime._IMClients[id] = promise;
}
realtime._registerPending(promise);
return promise;
};
Object.assign(realtime, {
register,
createIMClient,
});
/* eslint-enable no-param-reassign */
};
const beforeCommandDispatch = (command, realtime) => {
const isIMCommand = command.service === null || command.service === 2;
if (!isIMCommand) return true;
const targetClient = command.peerId
? realtime._IMClients[command.peerId]
: realtime._firstIMClient;
if (targetClient) {
Promise.resolve(targetClient)
.then(client => client._dispatchCommand(command))
.catch(debug);
} else {
debug(
'[WARN] Unexpected message received without any live client match: %O',
trim(command)
);
}
return false;
};
export const IMPlugin = {
name: 'leancloud-realtime-plugin-im',
onRealtimeCreate,
beforeCommandDispatch,
messageClasses: [Message, BinaryMessage, RecalledMessage, TextMessage],
};