UNPKG

9.95 kBJavaScriptView Raw
1/** @module leancloud-realtime */
2import d from 'debug';
3import uuid from 'uuid/v4';
4import IMClient from './im-client';
5import { RECONNECT, RECONNECT_ERROR } from './events/core';
6import { Conversation } from './conversations';
7import { MessageQueryDirection } from './conversations/conversation-base';
8import Message, { MessageStatus } from './messages/message';
9import BinaryMessage from './messages/binary-message';
10import TextMessage from './messages/text-message';
11import TypedMessage from './messages/typed-message';
12import RecalledMessage from './messages/recalled-message';
13import MessageParser from './message-parser';
14import { trim, internal, ensureArray, finalize } from './utils';
15
16const debug = d('LC:IMPlugin');
17
18/**
19 * 消息优先级枚举
20 * @enum {Number}
21 * @since 3.3.0
22 */
23const MessagePriority = {
24 /** 高 */
25 HIGH: 1,
26 /** 普通 */
27 NORMAL: 2,
28 /** 低 */
29 LOW: 3,
30};
31Object.freeze(MessagePriority);
32
33/**
34 * 为 Conversation 定义一个新属性
35 * @param {String} prop 属性名
36 * @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
37 * @returns void
38 * @example
39 *
40 * conversation.get('type');
41 * conversation.set('type', 1);
42 *
43 * // equals to
44 * defineConversationProperty('type');
45 * conversation.type;
46 * conversation.type = 1;
47 */
48const defineConversationProperty = (
49 prop,
50 descriptor = {
51 get() {
52 return this.get(prop);
53 },
54 set(value) {
55 this.set(prop, value);
56 },
57 }
58) => {
59 Object.defineProperty(Conversation.prototype, prop, descriptor);
60};
61
62export {
63 /**
64 * @see Message
65 */
66 Message,
67 /**
68 * @see BinaryMessage
69 */
70 BinaryMessage,
71 /**
72 * @see TypedMessage
73 */
74 TypedMessage,
75 /**
76 * @see TextMessage
77 */
78 TextMessage,
79 /**
80 * @see RecalledMessage
81 */
82 RecalledMessage,
83 MessagePriority,
84 MessageStatus,
85 MessageQueryDirection,
86 defineConversationProperty,
87};
88
89export {
90 /**
91 * decorator,定义消息类的类型常量
92 * @function
93 * @param {Number} type 自定义类型请使用正整数
94 * @example @messageType(1)
95 * class CustomMessage extends TypedMessage {}
96 *
97 * // 不支持 decorator 的情况下可以这样使用
98 * class CustomMessage extends TypedMessage {
99 * //...
100 * }
101 * messageType(1)(CustomMessage);
102 */
103 messageType,
104 /**
105 * decorator,定义消息类的自定义字段
106 * @function
107 * @param {String[]} fields 自定义字段
108 * @example @messageField(['foo'])
109 * class CustomMessage extends TypedMessage {
110 * constructor(foo) {
111 * super();
112 * this.foo = foo;
113 * }
114 * }
115 *
116 * // 不支持 decorator 的情况下可以这样使用
117 * class CustomMessage extends TypedMessage {
118 * constructor(foo) {
119 * super();
120 * this.foo = foo;
121 * }
122 * //...
123 * }
124 * messageField(['foo'])(CustomMessage);
125 */
126 messageField,
127 IE10Compatible,
128} from './messages/helpers';
129
130export { ConversationMemberRole } from './conversation-member-info';
131export {
132 /**
133 * @see Conversation
134 */
135 Conversation,
136 /**
137 * @see ChatRoom
138 */
139 ChatRoom,
140 /**
141 * @see ServiceConversation
142 */
143 ServiceConversation,
144 /**
145 * @see TemporaryConversation
146 */
147 TemporaryConversation,
148} from './conversations';
149
150const onRealtimeCreate = realtime => {
151 /* eslint-disable no-param-reassign */
152 const deviceId = uuid();
153 realtime._IMClients = {};
154 realtime._IMClientsCreationCount = 0;
155 const messageParser = new MessageParser(realtime._plugins);
156 realtime._messageParser = messageParser;
157
158 const signAVUser = async user =>
159 realtime._request({
160 method: 'POST',
161 path: '/rtm/sign',
162 data: {
163 session_token: user.getSessionToken(),
164 },
165 });
166
167 /**
168 * 注册消息类
169 *
170 * 在接收消息、查询消息时,会按照消息类注册顺序的逆序依次尝试解析消息内容
171 *
172 * @memberof Realtime
173 * @instance
174 * @param {Function | Function[]} messageClass 消息类,需要实现 {@link AVMessage} 接口,
175 * 建议继承自 {@link TypedMessage}
176 * @throws {TypeError} 如果 messageClass 没有实现 {@link AVMessage} 接口则抛出异常
177 */
178 const register = messageClass =>
179 ensureArray(messageClass).map(messageParser.register.bind(messageParser));
180 register(ensureArray(realtime._plugins.messageClasses));
181 /**
182 * 创建一个即时通讯客户端,多次创建相同 id 的客户端会返回同一个实例
183 * @memberof Realtime
184 * @instance
185 * @param {String|AV.User} [identity] 客户端 identity,如果不指定该参数,服务端会随机生成一个字符串作为 identity,
186 * 如果传入一个已登录的 AV.User,则会使用该用户的 id 作为客户端 identity 登录。
187 * @param {Object} [options]
188 * @param {Function} [options.signatureFactory] open session 时的签名方法 // TODO need details
189 * @param {Function} [options.conversationSignatureFactory] 对话创建、增减成员操作时的签名方法
190 * @param {Function} [options.blacklistSignatureFactory] 黑名单操作时的签名方法
191 * @param {String} [options.tag] 客户端类型标记,以支持单点登录功能
192 * @param {String} [options.isReconnect=false] 单点登录时标记该次登录是不是应用启动时自动重新登录
193 * @return {Promise.<IMClient>}
194 */
195 const createIMClient = async (
196 identity,
197 { tag, isReconnect, ...clientOptions } = {},
198 lagecyTag
199 ) => {
200 let id;
201 const buildinOptions = {};
202 if (identity) {
203 if (typeof identity === 'string') {
204 id = identity;
205 } else if (identity.id && identity.getSessionToken) {
206 ({ id } = identity);
207 const sessionToken = identity.getSessionToken();
208 if (!sessionToken) {
209 throw new Error('User must be authenticated');
210 }
211 buildinOptions.signatureFactory = signAVUser;
212 } else {
213 throw new TypeError('Identity must be a String or an AV.User');
214 }
215 if (realtime._IMClients[id] !== undefined) {
216 return realtime._IMClients[id];
217 }
218 }
219 if (lagecyTag) {
220 console.warn(
221 'DEPRECATION createIMClient tag param: Use options.tag instead.'
222 );
223 }
224 const _tag = tag || lagecyTag;
225 const promise = realtime
226 ._open()
227 .then(connection => {
228 const client = new IMClient(
229 id,
230 { ...buildinOptions, ...clientOptions },
231 {
232 _connection: connection,
233 _request: realtime._request.bind(realtime),
234 _messageParser: messageParser,
235 _plugins: realtime._plugins,
236 _identity: identity,
237 }
238 );
239 connection.on(RECONNECT, () =>
240 client
241 ._open(realtime._options.appId, _tag, deviceId, true)
242 /**
243 * 客户端连接恢复正常,该事件通常在 {@link Realtime#event:RECONNECT} 之后发生
244 * @event IMClient#RECONNECT
245 * @see Realtime#event:RECONNECT
246 * @since 3.2.0
247 */
248 /**
249 * 客户端重新登录发生错误(网络连接已恢复,但重新登录错误)
250 * @event IMClient#RECONNECT_ERROR
251 * @since 3.2.0
252 */
253 .then(
254 () => client.emit(RECONNECT),
255 error => client.emit(RECONNECT_ERROR, error)
256 )
257 );
258 internal(client)._eventemitter.on(
259 'beforeclose',
260 () => {
261 delete realtime._IMClients[client.id];
262 if (realtime._firstIMClient === client) {
263 delete realtime._firstIMClient;
264 }
265 },
266 realtime
267 );
268 internal(client)._eventemitter.on(
269 'close',
270 () => {
271 realtime._deregister(client);
272 },
273 realtime
274 );
275 return client
276 ._open(realtime._options.appId, _tag, deviceId, isReconnect)
277 .then(() => {
278 realtime._IMClients[client.id] = client;
279 realtime._IMClientsCreationCount += 1;
280 if (realtime._IMClientsCreationCount === 1) {
281 client._omitPeerId(true);
282 realtime._firstIMClient = client;
283 } else if (
284 realtime._IMClientsCreationCount > 1 &&
285 realtime._firstIMClient
286 ) {
287 realtime._firstIMClient._omitPeerId(false);
288 }
289 realtime._register(client);
290 return client;
291 })
292 .catch(error => {
293 delete realtime._IMClients[client.id];
294 throw error;
295 });
296 })
297 .then(
298 ...finalize(() => {
299 realtime._deregisterPending(promise);
300 })
301 );
302 if (identity) {
303 realtime._IMClients[id] = promise;
304 }
305 realtime._registerPending(promise);
306 return promise;
307 };
308 Object.assign(realtime, {
309 register,
310 createIMClient,
311 });
312 /* eslint-enable no-param-reassign */
313};
314
315const beforeCommandDispatch = (command, realtime) => {
316 const isIMCommand = command.service === null || command.service === 2;
317 if (!isIMCommand) return true;
318 const targetClient = command.peerId
319 ? realtime._IMClients[command.peerId]
320 : realtime._firstIMClient;
321 if (targetClient) {
322 Promise.resolve(targetClient)
323 .then(client => client._dispatchCommand(command))
324 .catch(debug);
325 } else {
326 debug(
327 '[WARN] Unexpected message received without any live client match: %O',
328 trim(command)
329 );
330 }
331 return false;
332};
333
334export const IMPlugin = {
335 name: 'leancloud-realtime-plugin-im',
336 onRealtimeCreate,
337 beforeCommandDispatch,
338 messageClasses: [Message, BinaryMessage, RecalledMessage, TextMessage],
339};