UNPKG

@parlour/parlour-client

Version:

The parlour websocket client

580 lines (484 loc) 12.3 kB
var parlour = (function (exports) { 'use strict'; class DebugTool { constructor() { this.open(); } open() { this._log = function () { console.log.apply(console, arguments); }; } close() { this._log = function () {}; } log() { this._log.apply(this, arguments); } } class Counter { constructor() { this.cnt = 0; } next() { let c = this.cnt; this.cnt++; return c; } } var Counter$1 = { create: () => { return new Counter(); } }; var ServerEvents = { Reply: 'phx_reply', Join: 'phx_join', Leave: 'phx_leave', Close: 'phx_close', Error: 'phx_error', PresenceDiff: 'presence_diff', PresenceState: 'presence_state', Ping: 'ping', Forward: 'forward', Presence: 'presence', Echo: 'echo', CreateRoom: 'create_room', RoomByAlias: 'room_by_alias', DismissRoom: 'dismiss_room', Rooms: 'rooms', RoomsByOwner: 'rooms_by_owner', SearchRooms: 'search_rooms', Heartbeat: 'heartbeat', Broadcast: 'broadcast', Cache: 'cache', CacheAdd: 'cache_add', CacheRemove: 'cache_rm', Timestamp: 'timestamp' }; class EventModel { constructor(data) { Object.assign(this, data); this._events = {}; } _on(name, fn) { const eventModel = this; if (!(name in eventModel._events)) { eventModel._events[name] = []; } eventModel._events[name].push(fn); return () => { eventModel.off(name, fn); }; } on() { const args = arguments; const typeOfArgs = typeof args[0]; switch (typeOfArgs) { case 'object': const eventHandlers = args[0]; const unbindFns = []; for (let key in eventHandlers) { unbindFns.push(this._on(key, eventHandlers[key])); } return () => { for (let i = 0; i < unbindFns.length; i++) { unbindFns[i](); } }; case 'string': return this._on(args[0], args[1]); } } off(name, fn) { const fns = this._events[name]; if (Array.isArray(fns)) { const index = fns.indexOf(fn); if (index >= 0) { fns.splice(index, 1); } } } trigger(name, args) { const fns = this._events[name]; if (Array.isArray(fns)) { for (let i = 0; i < fns.length; i++) { let fn = fns[i]; fn.apply(this, args); } } } } function delay(timeout) { timeout = timeout || 0; return new Promise(resolve => { setTimeout(() => { resolve(); }, timeout); }); } function getRoomChannel(roomId) { return `room:${roomId}`; } function getUserChannel(userId) { return `user:${userId}`; } function buildQueryString(queryStringsMap) { let qsArr = []; for (let key in queryStringsMap) { qsArr.push(`${key}=${queryStringsMap[key]}`); } return qsArr.join('&'); } class SocketClient extends EventModel { constructor(data) { super(data); this.externalQueryString = this.externalQueryString || {}; this.ping = 0; this.refMap = {}; this.rooms = []; } get userChannel() { return getUserChannel(this.userId); } get isConnected() { return this.socket && this.socket.readyState === WebSocket.OPEN; } get isConnecting() { return this.socket && this.socket.readyState === WebSocket.CONNECTING; } get isClosing() { return this.socket && this.socket.readyState === WebSocket.CLOSING; } get isClosed() { return this.socket && this.socket.readyState === WebSocket.CLOSED; } _handle(data) { const messageHandler = this.messageHandler; const jsonData = JSON.parse(data); const handler = messageHandler[jsonData.event] || messageHandler.default; handler(this, jsonData.event, jsonData.topic, jsonData.ref, jsonData.payload); } connect() { const Debug = this.Debug; if (!this.socket || this.isClosed) { const queryStrings = { userId: this.userId, userName: this.userName, token: this.token, ...this.externalQueryString }; const queryStringText = buildQueryString(queryStrings); const socket = this.socket = new WebSocket(`${this.url}?${queryStringText}`); socket.onopen = e => { // Debug.log('join room', this.room, this.userChannel); this.refMap = {}; for (let i in this.rooms) { let room = this.rooms[i]; this.join(room); } this.join(this.userChannel); this.startHeartbeat(); Debug.log('[Socket] Open', e); this.trigger('connect'); }; socket.onclose = e => { this.stopHeartbeat(); Debug.log('[Socket] Close', e); this.trigger('close', [e]); if (this._isReconnectFlowEnabled) { delete this._isReconnectFlowEnabled; this.connect(); } else if (typeof this.reconnectPeriod === 'number') { const client = this; (async () => { Debug.log('Reconnect auto...'); await delay(client.reconnectPeriod); client.connect(); Debug.log('....done'); })(); } }; socket.onmessage = e => { Debug.log('[Socket] Message', e.data); this._handle(e.data); }; socket.onerror = e => { Debug.log('[Socket] Error', e); this.trigger('error', [e]); }; } } end() { if (this.socket) { this.socket.close(); } } reconnect() { this._isReconnectFlowEnabled = true; this.end(); } startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setTimeout(async () => { try { if (this.isConnected) { const resp = await this.heartbeat(this.userChannel); const ping = Date.now() - resp.response.payload.t; this.ping = ping; this.trigger(ServerEvents.Ping, [this.ping]); } } catch (err) {// failed to update ping value } finally { this.startHeartbeat(); } }, this.heartbeatInterval); } stopHeartbeat() { if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); delete this.heartbeatTimer; } } /* API。*/ responseRef(ref, payload) { const handler = this.refMap[ref]; if (handler) { delete this.refMap[ref]; handler.done(payload); } } send({ event, topic, payload } = {}) { const ref = this.refCounter.next().toString(); const data = { event: event, topic: topic, payload: payload || '', ref: ref }; if (event === ServerEvents.Join || event === ServerEvents.Leave) { data.join_ref = this.refJoinCounter.next(); } this.socket.send(JSON.stringify(data)); const refMap = this.refMap; const refHandler = refMap[ref] = {}; return new Promise(resolve => { refHandler.done = payload => { resolve(payload); }; }); } join(topic, payload) { return this.send({ event: ServerEvents.Join, topic: topic, payload: payload }); } leave(topic, payload) { return this.send({ event: ServerEvents.Leave, topic: topic, payload: payload }); } heartbeat(topic) { return this.send({ event: ServerEvents.Heartbeat, topic: topic, payload: { t: Date.now() } }); } broadcastToRoom(roomId, event, payload) { return this.send({ event: ServerEvents.Broadcast, topic: getRoomChannel(roomId), payload: { event: event, payload: payload } }); } echo(topic, payload) { return this.send({ event: ServerEvents.Echo, topic: topic, payload: payload }); } presence(topic) { return this.send({ event: ServerEvents.Presence, topic: topic }); } sendMessageTo(userId, event, payload) { return this.send({ event: ServerEvents.Forward, topic: this.userChannel, payload: { userId: getUserChannel(userId), event: event, payload: payload } }); } /* * alias: "" * name: "" * password: "" * subject: "" * cache: {} */ createRoom(data) { return this.send({ event: ServerEvents.CreateRoom, topic: this.userChannel, payload: { info: data } }); } dismissRoom(roomId) { return this.send({ event: ServerEvents.DismissRoom, topic: this.userChannel, payload: { id: roomId } }); } roomByAlias(alias) { return this.send({ event: ServerEvents.RoomByAlias, topic: this.userChannel, payload: { alias: alias } }); } getRooms() { return this.send({ event: ServerEvents.Rooms, topic: this.userChannel, payload: {} }); } getRoomsByOwner() { return this.send({ event: ServerEvents.RoomsByOwner, topic: this.userChannel, payload: {} }); } /* * queryData: { subject: "" } */ searchRooms(queryData) { return this.send({ event: ServerEvents.SearchRooms, topic: this.userChannel, payload: queryData }); } joinRoom(id, password) { const index = this.rooms.indexOf(id); if (index < 0) { this.rooms.push(id); } return this.join(getRoomChannel(id), { password: password || '' }); } joinLobby() { return this.joinRoom('lobby'); } leaveRoom(id) { const index = this.rooms.indexOf(id); if (index >= 0) { this.rooms.splice(index, 1); } return this.leave(getRoomChannel(id)); } leaveLobby() { return this.leaveRoom('lobby'); } getCache(roomId) { return this.send({ event: ServerEvents.Cache, topic: getRoomChannel(roomId), payload: {} }); } addCache(roomId, key, value) { return this.send({ event: ServerEvents.CacheAdd, topic: getRoomChannel(roomId), payload: { key, value } }); } removeCache(roomId, key) { return this.send({ event: ServerEvents.CacheRemove, topic: getRoomChannel(roomId), payload: { key } }); } getServerTime() { return this.send({ event: ServerEvents.Timestamp, topic: this.userChannel, payload: {} }); } } var entry = { create: params => { let socketClient; function createMessageHandler() { return { [ServerEvents.Reply]: (client, event, topic, ref, payload) => { // Debug.log('[Reply] ', topic, ref, payload); socketClient.responseRef(ref, payload); client.trigger('reply', [topic, event, payload]); }, [ServerEvents.PresenceDiff]: (client, event, topic, ref, payload) => { // Debug.log('[PresenceDiff] ', topic, ref, payload); client.trigger('presence_diff', [topic, event, payload]); }, [ServerEvents.PresenceState]: (client, event, topic, ref, payload) => { // Debug.log('[PresenceState] ', topic, ref, payload); client.trigger('presence_state', [topic, event, payload]); }, 'default': (client, event, topic, ref, payload) => { // Debug.log('[Default] ', event, topic, ref, payload); client.trigger('message', [topic, event, payload]); } }; } const messageHandler = createMessageHandler(); socketClient = new SocketClient(Object.assign(params, { messageHandler: messageHandler, Debug: new DebugTool(), refCounter: Counter$1.create(), refJoinCounter: Counter$1.create(), heartbeatInterval: 11000 })); return socketClient; } }; exports.SocketClient = entry; Object.defineProperty(exports, '__esModule', { value: true }); return exports; }({}));