@parlour/parlour-client
Version:
The parlour websocket client
575 lines (481 loc) • 11.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
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;