UNPKG

symple-client

Version:

Symple realtime messaging client

499 lines (442 loc) 15.9 kB
const Symple = require('./symple'); const { io } = require('socket.io-client'); // (function (S) { // Symple client class Symple.Client = Symple.Emitter.extend({ init: function (options) { this.options = Symple.extend({ url: options.url ? options.url : 'http://localhost:4000', secure: !!(options.url && (options.url.indexOf('https') === 0 || options.url.indexOf('wss') === 0)), token: undefined, // pre-arranged server session token peer: {} }, options) this._super() this.options.auth = Symple.extend({ token: this.options.token || '', user: this.options.peer.user || '', name: this.options.peer.name || '', type: this.options.peer.type || '' }, this.options.auth) this.peer = options.peer // Symple.extend(this.options.auth, options.peer) this.peer.rooms = this.peer.rooms || [] // delete this.peer.token this.roster = new Symple.Roster(this) this.socket = null }, // Connects and authenticates on the server. // If the server is down the 'error' event will fire. connect: function () { Symple.log('symple:client: connecting', this.options) var self = this if (this.socket) { throw 'The client socket is not null' } // var io = io || window.io // console.log(io) // this.options.auth || {} // this.options.auth.user = this.peer.user // this.options.auth.token = this.options.token this.socket = io.connect(this.options.url, this.options) this.socket.on('connect', function () { Symple.log('symple:client: connected') self.peer.id = self.socket.id self.peer.online = true self.roster.add(self.peer) setTimeout(function () { self.sendPresence({ probe: true }) }) // next iteration incase rooms are joined on connect self.emit('connect') self.socket.on('message', function (m) { Symple.log('symple:client: receive', m); if (typeof (m) === 'object') { switch (m.type) { case 'message': m = new Symple.Message(m) break case 'command': m = new Symple.Command(m) break case 'event': m = new Symple.Event(m) break case 'presence': m = new Symple.Presence(m) if (m.data.online) { self.roster.update(m.data) } else { setTimeout(function () { // remove after timeout self.roster.remove(m.data.id) }) } if (m.probe) { self.sendPresence(new Symple.Presence({ to: Symple.parseAddress(m.from).id })) } break default: m.type = m.type || 'message' break } if (typeof (m.from) === 'string') { // Replace the from attribute with the full peer object. // This will only work for peer messages, not server messages. var rpeer = self.roster.get(m.from) if (rpeer) { m.from = rpeer } else { Symple.log('symple:client: got message from unknown peer', m) } } // Dispatch to the application self.emit(m.type, m) } }) }) this.socket.on('error', function (error) { // This is triggered when any transport fails, // so not necessarily fatal. self.emit('error', error) }) this.socket.on('connecting', function () { Symple.log('symple:client: connecting') self.emit('connecting') }) this.socket.on('reconnecting', function () { Symple.log('symple:client: reconnecting') self.emit('reconnecting') }) this.socket.on('connect_error', (error) => { // Called when authentication middleware fails self.emit('connect_error') self.setError('auth', error.message) Symple.log('symple:client: connect error', error) }) this.socket.on('connect_failed', function () { // Called when all transports fail Symple.log('symple:client: connect failed') self.emit('connect_failed') self.setError('connect') }) this.socket.on('disconnect', function (reason) { Symple.log('symple:client: disconnect', reason) self.peer.online = false self.emit('disconnect') }) }, // Disconnect from the server disconnect: function () { if (this.socket) { this.socket.disconnect() } }, // Return the online status online: function () { return this.peer.online }, // Join a room join: function (room) { this.socket.emit('join', room) }, // Leave a room leave: function (room) { this.socket.emit('leave', room) }, // Send a message to the given peer send: function (m, to) { // Symple.log('symple:client: before send', m, to); if (!this.online()) { throw 'Cannot send messages while offline' } // add to pending queue? if (typeof (m) !== 'object') { throw 'Message must be an object' } if (typeof (m.type) !== 'string') { m.type = 'message' } if (!m.id) { m.id = Symple.randomString(8) } if (to) { m.to = to } if (m.to && typeof (m.to) === 'object') { m.to = Symple.buildAddress(m.to) } if (m.to && typeof (m.to) !== 'string') { throw 'Message `to` attribute must be an address string' } m.from = Symple.buildAddress(this.peer) if (m.from === m.to) { throw 'Message sender cannot match the recipient' } Symple.log('symple:client: sending', m) this.socket.emit('message', m) // this.socket.json.send(m) }, respond: function (m) { this.send(m, m.from) }, sendMessage: function (m, to) { this.send(m, to) }, sendPresence: function (p) { p = p || {} if (p.data) { p.data = Symple.merge(this.peer, p.data) } else { p.data = this.peer } this.send(new Symple.Presence(p)) }, sendCommand: function (c, to, fn, once) { var self = this c = new Symple.Command(c, to) this.send(c) if (fn) { this.onResponse('command', { id: c.id }, fn, function (res) { // NOTE: 202 (Accepted) and 406 (Not acceptable) response codes // signal that the command has not yet completed. if (once || (res.status !== 202 && res.status !== 406)) { self.clear('command', fn) } }) } }, // Adds a capability for our current peer addCapability: function (name, value) { var peer = this.peer if (peer) { if (typeof value === 'undefined') { value = true } if (typeof peer.capabilities === 'undefined') { peer.capabilities = {} } peer.capabilities[name] = value // var idx = peer.capabilities.indexOf(name); // if (idx === -1) { // peer.capabilities.push(name); // this.sendPresence(); // } } }, // Removes a capability from our current peer removeCapability: function (name) { var peer = this.peer if (peer && typeof peer.capabilities !== 'undefined' && typeof peer.capabilities[name] !== 'undefined') { delete peer.capabilities[key] this.sendPresence() // var idx = peer.capabilities.indexOf(name) // if (idx !== -1) { // peer.capabilities.pop(name); // this.sendPresence(); // } } }, // Checks if a peer has a specific capbility and returns a boolean hasCapability: function (id, name) { var peer = this.roster.get(id) if (peer) { if (typeof peer.capabilities !== 'undefined' && typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] !== false } if (typeof peer.data !== 'undefined' && typeof peer.data.capabilities !== 'undefined' && typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] !== false } } return false }, // Checks if a peer has a specific capbility and returns the value getCapability: function (id, name) { var peer = this.roster.get(id) if (peer) { if (typeof peer.capabilities !== 'undefined' && typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] } if (typeof peer.data !== 'undefined' && typeof peer.data.capabilities !== 'undefined' && typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] } } return undefined }, // Sets the client to an error state and disconnect setError: function (error, message) { Symple.log('symple:client: fatal error', error, message) // if (this.error === error) // return; // this.error = error; this.emit('error', error, message) if (this.socket) { this.socket.disconnect() } }, onResponse: function (event, filters, fn, after) { if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } if (typeof fn !== 'undefined' && fn.constructor === Function) { this.listeners[event].push({ fn: fn, // data callback function after: after, // after data callback function filters: filters // event filter object for matching response }) } }, clear: function (event, fn) { Symple.log('symple:client: clearing callback', event) if (typeof this.listeners[event] !== 'undefined') { for (var i = 0; i < this.listeners[event].length; i++) { if (this.listeners[event][i].fn === fn && String(this.listeners[event][i].fn) === String(fn)) { this.listeners[event].splice(i, 1) Symple.log('symple:client: cleared callback', event) } } } }, // Extended emit function to handle filtered message response // callbacks first, and then standard events. emit: function () { if (!this.emitResponse.apply(this, arguments)) { this._super.apply(this, arguments) } }, // Emit function for handling filtered message response callbacks. emitResponse: function () { var event = arguments[0] var data = Array.prototype.slice.call(arguments, 1) if (typeof this.listeners[event] !== 'undefined') { for (var i = 0; i < this.listeners[event].length; i++) { if (typeof this.listeners[event][i] === 'object' && this.listeners[event][i].filters !== 'undefined' && Symple.match(this.listeners[event][i].filters, data[0])) { this.listeners[event][i].fn.apply(this, data) if (this.listeners[event][i].after !== 'undefined') { this.listeners[event][i].after.apply(this, data) } return true } } } return false } // getPeers: function(fn) { // var self = this; // this.socket.emit('peers', function(res) { // Symple.log('Peers: ', res); // if (typeof(res) !== 'object') // for (var peer in res) // self.roster.update(peer); // if (fn) // fn(res); // }); // } }) // ------------------------------------------------------------------------- // Symple Roster // Symple.Roster = Symple.Manager.extend({ init: function (client) { this._super() this.client = client }, // Add a peer object to the roster add: function (peer) { Symple.log('symple:roster: adding', peer) if (!peer || !peer.id || !peer.user) { throw 'Cannot add invalid peer' } this._super(peer) this.client.emit('addPeer', peer) }, // Remove the peer matching an ID or address string: user|id remove: function (id) { id = Symple.parseAddress(id).id || id var peer = this._super(id) Symple.log('symple:roster: removing', id, peer) if (peer) { this.client.emit('removePeer', peer) } return peer }, // Get the peer matching an ID or address string: user|id get: function (id) { // Handle IDs peer = this._super(id) // id = Symple.parseIDFromAddress(id) || id; if (peer) { return peer } // Handle address strings return this.findOne(Symple.parseAddress(id)) }, update: function (data) { if (!data || !data.id) { return } var peer = this.get(data.id) if (peer) { for (var key in data) { peer[key] = data[key] } } else { this.add(data) } } // Get the peer matching an address string: user|id // getForAddr: function(addr) { // var o = Symple.parseAddress(addr); // if (o && o.id) // return this.get(o.id); // return null; // } }) // ------------------------------------------------------------------------- // Message // Symple.Message = function (json) { if (typeof (json) === 'object') { this.fromJSON(json) } this.type = 'message' } Symple.Message.prototype = { fromJSON: function (json) { for (var key in json) { this[key] = json[key] } }, valid: function () { return this['id'] && this['from'] } } // ------------------------------------------------------------------------- // Command // Symple.Command = function (json) { if (typeof (json) === 'object') { this.fromJSON(json) } this.type = 'command' } Symple.Command.prototype = { getData: function (name) { return this['data'] ? this['data'][name] : null }, params: function () { return this['node'].split(':') }, param: function (n) { return this.params()[n - 1] }, matches: function (xuser) { xparams = xuser.split(':') // No match if x params are greater than ours. if (xparams.length > this.params().length) { return false } for (var i = 0; i < xparams.length; i++) { // Wildcard * matches everything until next parameter. if (xparams[i] === '*') { continue } if (xparams[i] !== this.params()[i]) { return false } } return true }, fromJSON: function (json) { for (var key in json) { this[key] = json[key] } }, valid: function () { return this['id'] && this['from'] && this['node'] } } // ------------------------------------------------------------------------- // Presence // Symple.Presence = function (json) { if (typeof (json) === 'object') { this.fromJSON(json) } this.type = 'presence' } Symple.Presence.prototype = { fromJSON: function (json) { for (var key in json) { this[key] = json[key] } }, valid: function () { return this['id'] && this['from'] } } // ------------------------------------------------------------------------- // Event // Symple.Event = function (json) { if (typeof (json) === 'object') { this.fromJSON(json) } this.type = 'event' } Symple.Event.prototype = { fromJSON: function (json) { for (var key in json) { this[key] = json[key] } }, valid: function () { return this['id'] && this['from'] && this.name } } // })(window.Symple = window.Symple || {}) /** * Module exports. */ module.exports = Symple;