UNPKG

beamzer-client

Version:

This wrapper script makes use of the JS polyfill for Server Sent Events OR Server Push

1,357 lines (1,059 loc) 44.6 kB
/* * EventSource polyfill version {{VERSION}} * Supported by sc AmvTek srl * :email: devel@amvtek.com */ ;(function (global) { if (global.EventSource && !global._eventSourceImportPrefix){ return; } var evsImportName = (global._eventSourceImportPrefix||'')+"EventSource"; var EventSource = function (url, options) { if (!url || typeof url != 'string') { throw new SyntaxError('Not enough arguments'); } this.URL = url; this.setOptions(options); var evs = this; setTimeout(function(){evs.poll()}, 0); }; EventSource.prototype = { CONNECTING: 0, OPEN: 1, CLOSED: 2, defaultOptions: { loggingEnabled: false, loggingPrefix: "eventsource", interval: 500, // milliseconds bufferSizeLimit: 256*1024, // bytes silentTimeout: 300000, // milliseconds getArgs:{ 'evs_buffer_size_limit': 256*1024 }, xhrHeaders:{ 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Requested-With': 'XMLHttpRequest' } }, setOptions: function(options){ var defaults = this.defaultOptions; var option; // set all default options... for (option in defaults){ if ( defaults.hasOwnProperty(option) ){ this[option] = defaults[option]; } } // override with what is in options for (option in options){ if (option in defaults && options.hasOwnProperty(option)){ this[option] = options[option]; } } // if getArgs option is enabled // ensure evs_buffer_size_limit corresponds to bufferSizeLimit if (this.getArgs && this.bufferSizeLimit) { this.getArgs['evs_buffer_size_limit'] = this.bufferSizeLimit; } // if console is not available, force loggingEnabled to false if (typeof console === "undefined" || typeof console.log === "undefined") { this.loggingEnabled = false; } }, log: function(message) { if (this.loggingEnabled) { console.log("[" + this.loggingPrefix +"]:" + message) } }, poll: function() { try { if (this.readyState == this.CLOSED) { return; } this.cleanup(); this.readyState = this.CONNECTING; this.cursor = 0; this.cache = ''; this._xhr = new this.XHR(this); this.resetNoActivityTimer(); } catch (e) { // in an attempt to silence the errors this.log('There were errors inside the pool try-catch'); this.dispatchEvent('error', { type: 'error', data: e.message }); } }, pollAgain: function (interval) { // schedule poll to be called after interval milliseconds var evs = this; evs.readyState = evs.CONNECTING; evs.dispatchEvent('error', { type: 'error', data: "Reconnecting " }); this._pollTimer = setTimeout(function(){evs.poll()}, interval||0); }, cleanup: function() { this.log('evs cleaning up') if (this._pollTimer){ clearInterval(this._pollTimer); this._pollTimer = null; } if (this._noActivityTimer){ clearInterval(this._noActivityTimer); this._noActivityTimer = null; } if (this._xhr){ this._xhr.abort(); this._xhr = null; } }, resetNoActivityTimer: function(){ if (this.silentTimeout){ if (this._noActivityTimer){ clearInterval(this._noActivityTimer); } var evs = this; this._noActivityTimer = setTimeout( function(){ evs.log('Timeout! silentTImeout:'+evs.silentTimeout); evs.pollAgain(); }, this.silentTimeout ); } }, close: function () { this.readyState = this.CLOSED; this.log('Closing connection. readyState: '+this.readyState); this.cleanup(); }, _onxhrdata: function() { var request = this._xhr; if (request.isReady() && !request.hasError() ) { // reset the timer, as we have activity this.resetNoActivityTimer(); // move this EventSource to OPEN state... if (this.readyState == this.CONNECTING) { this.readyState = this.OPEN; this.dispatchEvent('open', { type: 'open' }); } var buffer = request.getBuffer(); if (buffer.length > this.bufferSizeLimit) { this.log('buffer.length > this.bufferSizeLimit'); this.pollAgain(); } if (this.cursor == 0 && buffer.length > 0){ // skip byte order mark \uFEFF character if it starts the stream if (buffer.substring(0,1) == '\uFEFF'){ this.cursor = 1; } } var lastMessageIndex = this.lastMessageIndex(buffer); if (lastMessageIndex[0] >= this.cursor){ var newcursor = lastMessageIndex[1]; var toparse = buffer.substring(this.cursor, newcursor); this.parseStream(toparse); this.cursor = newcursor; } // if request is finished, reopen the connection if (request.isDone()) { this.log('request.isDone(). reopening the connection'); this.pollAgain(this.interval); } } else if (this.readyState !== this.CLOSED) { this.log('this.readyState !== this.CLOSED'); this.pollAgain(this.interval); //MV: Unsure why an error was previously dispatched } }, parseStream: function(chunk) { // normalize line separators (\r\n,\r,\n) to \n // remove white spaces that may precede \n chunk = this.cache + this.normalizeToLF(chunk); var events = chunk.split('\n\n'); var i, j, eventType, datas, line, retry; for (i=0; i < (events.length - 1); i++) { eventType = 'message'; datas = []; parts = events[i].split('\n'); for (j=0; j < parts.length; j++) { line = this.trimWhiteSpace(parts[j]); if (line.indexOf('event') == 0) { eventType = line.replace(/event:?\s*/, ''); } else if (line.indexOf('retry') == 0) { retry = parseInt(line.replace(/retry:?\s*/, '')); if(!isNaN(retry)) { this.interval = retry; } } else if (line.indexOf('data') == 0) { datas.push(line.replace(/data:?\s*/, '')); } else if (line.indexOf('id:') == 0) { this.lastEventId = line.replace(/id:?\s*/, ''); } else if (line.indexOf('id') == 0) { // this resets the id this.lastEventId = null; } } if (datas.length) { // dispatch a new event var event = new MessageEvent(eventType, datas.join('\n'), window.location.origin, this.lastEventId); this.dispatchEvent(eventType, event); } } this.cache = events[events.length - 1]; }, dispatchEvent: function (type, event) { var handlers = this['_' + type + 'Handlers']; if (handlers) { for (var i = 0; i < handlers.length; i++) { handlers[i].call(this, event); } } if (this['on' + type]) { this['on' + type].call(this, event); } }, addEventListener: function (type, handler) { if (!this['_' + type + 'Handlers']) { this['_' + type + 'Handlers'] = []; } this['_' + type + 'Handlers'].push(handler); }, removeEventListener: function (type, handler) { var handlers = this['_' + type + 'Handlers']; if (!handlers) { return; } for (var i = handlers.length - 1; i >= 0; --i) { if (handlers[i] === handler) { handlers.splice(i, 1); break; } } }, _pollTimer: null, _noactivityTimer: null, _xhr: null, lastEventId: null, cache: '', cursor: 0, onerror: null, onmessage: null, onopen: null, readyState: 0, // =================================================================== // helpers functions // those are attached to prototype to ease reuse and testing... urlWithParams: function (baseURL, params) { var encodedArgs = []; if (params){ var key, urlarg; var urlize = encodeURIComponent; for (key in params){ if (params.hasOwnProperty(key)) { urlarg = urlize(key)+'='+urlize(params[key]); encodedArgs.push(urlarg); } } } if (encodedArgs.length > 0){ if (baseURL.indexOf('?') == -1) return baseURL + '?' + encodedArgs.join('&'); return baseURL + '&' + encodedArgs.join('&'); } return baseURL; }, lastMessageIndex: function(text) { var ln2 =text.lastIndexOf('\n\n'); var lr2 = text.lastIndexOf('\r\r'); var lrln2 = text.lastIndexOf('\r\n\r\n'); if (lrln2 > Math.max(ln2, lr2)) { return [lrln2, lrln2+4]; } return [Math.max(ln2, lr2), Math.max(ln2, lr2) + 2] }, trimWhiteSpace: function(str) { // to remove whitespaces left and right of string var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; return str.replace(reTrim, ''); }, normalizeToLF: function(str) { // replace \r and \r\n with \n return str.replace(/\r\n|\r/g, '\n'); } }; if (!isOldIE()){ EventSource.isPolyfill = "XHR"; // EventSource will send request using XMLHttpRequest EventSource.prototype.XHR = function(evs) { request = new XMLHttpRequest(); this._request = request; evs._xhr = this; // set handlers request.onreadystatechange = function(){ if (request.readyState > 1 && evs.readyState != evs.CLOSED) { if (request.status == 200 || (request.status>=300 && request.status<400)){ evs._onxhrdata(); } else { request._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "The server responded with "+request.status }); evs.close(); } } }; request.onprogress = function () { }; request.open('GET', evs.urlWithParams(evs.URL, evs.getArgs), true); var headers = evs.xhrHeaders; // maybe null for (var header in headers) { if (headers.hasOwnProperty(header)){ request.setRequestHeader(header, headers[header]); } } if (evs.lastEventId) { request.setRequestHeader('Last-Event-Id', evs.lastEventId); } request.send(); }; EventSource.prototype.XHR.prototype = { useXDomainRequest: false, _request: null, _failed: false, // true if we have had errors... isReady: function() { return this._request.readyState >= 2; }, isDone: function() { return (this._request.readyState == 4); }, hasError: function() { return (this._failed || (this._request.status >= 400)); }, getBuffer: function() { var rv = ''; try { rv = this._request.responseText || ''; } catch (e){} return rv; }, abort: function() { if ( this._request ) { this._request.abort(); } } }; } else { EventSource.isPolyfill = "IE_8-9"; // patch EventSource defaultOptions var defaults = EventSource.prototype.defaultOptions; defaults.xhrHeaders = null; // no headers will be sent defaults.getArgs['evs_preamble'] = 2048 + 8; // EventSource will send request using Internet Explorer XDomainRequest EventSource.prototype.XHR = function(evs) { request = new XDomainRequest(); this._request = request; // set handlers request.onprogress = function(){ request._ready = true; evs._onxhrdata(); }; request.onload = function(){ this._loaded = true; evs._onxhrdata(); }; request.onerror = function(){ this._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "XDomainRequest error" }); }; request.ontimeout = function(){ this._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "XDomainRequest timed out" }); }; // XDomainRequest does not allow setting custom headers // If EventSource has enabled the use of GET arguments // we add parameters to URL so that server can adapt the stream... var reqGetArgs = {}; if (evs.getArgs) { // copy evs.getArgs in reqGetArgs var defaultArgs = evs.getArgs; for (var key in defaultArgs) { if (defaultArgs.hasOwnProperty(key)){ reqGetArgs[key] = defaultArgs[key]; } } if (evs.lastEventId){ reqGetArgs['evs_last_event_id'] = evs.lastEventId; } } // send the request request.open('GET', evs.urlWithParams(evs.URL,reqGetArgs)); request.send(); }; EventSource.prototype.XHR.prototype = { useXDomainRequest: true, _request: null, _ready: false, // true when progress events are dispatched _loaded: false, // true when request has been loaded _failed: false, // true if when request is in error isReady: function() { return this._request._ready; }, isDone: function() { return this._request._loaded; }, hasError: function() { return this._request._failed; }, getBuffer: function() { var rv = ''; try { rv = this._request.responseText || ''; } catch (e){} return rv; }, abort: function() { if ( this._request){ this._request.abort(); } } }; } function MessageEvent(type, data, origin, lastEventId) { this.bubbles = false; this.cancelBubble = false; this.cancelable = false; this.data = data || null; this.origin = origin || ''; this.lastEventId = lastEventId || ''; this.type = type || 'message'; } function isOldIE () { //return true if we are in IE8 or IE9 return (global.XDomainRequest && (global.XMLHttpRequest && (new global.XMLHttpRequest()).responseType === undefined)) ? true : false; } global[evsImportName] = EventSource; })((this || self)); /** * * Project: BeamzerClient * License: MIT * Author: Ifeora Okechukwu (isocroft@gmail.com) * * Covers: IE8.0+, FF3.5+, Chrm2.0+, Saf3.0+, Opr5.0+ * * This wrapper script makes use of the JS polyfill * located at https://github.com/amvtek/EventSource/ * * Polyfill Dependency Author: devel@amvtek.com */ /*! * Example: * * var beam = new BeamzerClient({ * source:"http://www.example.com/beamrays", * params:{ * choords:["$x", "$y"], * id:"id" * }, options:{loggingEnabled:true, crossdomain:true, interval:4500} * }); * * beam.start(function onOpen(e){ }, onfunction onError(e){ }, function onMessage(e){ }); * beam.on("update", function(e){ }); * beam.on("noupdate", function(e){ }); * beam.newClient({source:"http://www.example.com/beamrays",params:{},options:{}}); * beam.off("update"); * beam.stop(function(e){ }); * */ // @see https://github.com/amvtek/EventSource/blob/master/javascript/src/eventsource.js ;(function(global, factory, undefined){ // filling up JS object(s) API where missing Array.prototype.indexOf = Array.prototype.indexOf || function(needle){ var array = this, length = array.length; for(var i = 0; i < length; i++){ if(needle === array[i]){ return i; } } return -1; }; Object.keys = Object.keys || function(object){ var property, keys = []; for(property in object){ if((!!object.hasOwnProperty) && object.hasOwnProperty(property)){ keys.push(property); } } return keys; } // UMD if(typeof module != 'undefined' && (!!module.exports)){ module.exports = factory(); }else if(typeof define != 'undefined' && (!!define.amd)){ define("BeamzerClient", factory); }else if(global !== undefined && (global.window === global)){ global.BeamzerClient = factory(global); } }((this || self), function(win){ // load the shim so it polyfills as "EventSource" on the global object // for browsers that don't natively support "EventSource" constructor if(!win.EventSource){ return; } var _arr = ([]), _obj = ({}), _noop = function(){}, beamzerOrigin = 'https://app.beamzer.co', _url_obj = "[object URL]", _arr_obj = "[object Array]", _func_obj = "[object Function]", _obj_obj = "[object Object]", _oops = [''], toString = _obj.toString, hasOWnProp = _obj.hasOwnProperty, /* doc = win.document, */ loc = win.location, isOldBrowser = !function(w){ try{ /* try to detect a browser that doesn't properly recognize "functions" because it sees them as "objects" e.g. fr**kin' stupid IE8! */ var timeout = w.setTimeout.apply(w, _noop); }catch(ex){ return true; } return false; }(win), regexFunctionName = /^function(?:[\s]*)(.+(?=\())/i, _origin = (loc.origin + '/') || (loc.protocol + '//' + loc.host + (loc.port || "") + '/'), open_or_error = /^(?:on|)?(?:open|error)/i, rgx_url = /^https?\:\/\/(?:([\w]+?)\.){1,2}(?:com?|org|co\.uk|ex|ca|co\.za|net|ng|com\.ng|gov|mil|biz|info|io|me|dev|ng|mobi|name|edu|nti|aero|jobs|museumco\.za)\/?/, _clientsObject = null, _instance = null, _each = function(obj, callback){ for(var prop in obj){ if(hasOWnProp.call(obj, prop)){ callback(obj[prop], prop, obj); } } }, _isShim = (isOldBrowser || ('poll' in win.EventSource.prototype) && (typeof(win.EventSource.prototype.poll) === "function")), connections = {}, EventsRegistry = (function registry(){ var eventsMap = {}; var proxyObserverList = {}; function addGlobalHandler(object){ var _wrap = function(callable){ return function(e, url){ if(e.target.readyState === EventSource.CLOSED){ if(typeof this.close == "function"){ //this.close(); callable.apply(null, [e, url]); if(!!win.console){ win.console.log("[BeamzerClient]: Server-Sent Event Stream Closed!"); } } }else if(e.target.readyState === EventSource.CONNECTING){ if(!!win.console){ win.console.log("[BeamzerClient]: Server-Sent Event Stream Connecting..."); } callable.apply(this, [e, url]); } } }; // adding events and their handlers from the global map only _each(object, function(handler, event){ var prefix = '', _event; switch(event){ case 'open': case 'error': case 'message': prefix = 'on'; break; } _event = prefix + event; if(!hasOWnProp.call(eventsMap, _event)){ if(typeof handler == "function"){ eventsMap[_event] = _wrap(handler); } } }); } function removeGlobalHandler(event){ // removing events and their handlers from the global map only if(hasOWnProp.call(eventsMap, event)){ delete eventsMap[event]; return true; } return false; } function readyProxyObserver(event, url, noCall){ var proxy; if(!hasOWnProp.call(proxyObserverList, url)){ proxyObserverList[url] = { events:[], callback:function(e){ var _self = this; e = e || {} // hoisting... _each(eventsMap, function(item, key){ if(key.indexOf((e.type || event)) > -1){ if((!noCall) && (key.match(open_or_error) || true)){ eventsMap[key].call(_self, e, url); } } }); } }; } proxy = proxyObserverList[url]; if(proxy && proxy.events && typeof proxy.events.push === 'function') { proxy.events.push(event); } return proxy.callback; } function getProxy(url){ if(hasOWnProp.call(proxyObserverList, url)){ return proxyObserverList[url]; } return null; } function getEvents(){ return eventsMap; } return { getEvents:getEvents, getProxy:getProxy, readyProxyObserver:readyProxyObserver, removeGlobalHandler:removeGlobalHandler, addGlobalHandler:addGlobalHandler }; }), _installEvents = function(object, noCall){ var evts = EventsRegistry.getEvents(), _evts = Object.keys(evts), _event; if(toString.call(object.constructor) !== _func_obj){ _each(object, function(connection, url){ _each(evts, function(callback, event){ if(event.indexOf('on') === 0){ _event = event.replace('on', ''); if(connection[event] === null) connection[event] = EventsRegistry.readyProxyObserver(_event, url, noCall); }else{ if(_evts.indexOf(event) == -1) connection.addEventListener(event, EventsRegistry.readyProxyObserver(event, url, noCall), false); } }); }); }else{ _each(evts, function(callback, event){ if(event.indexOf('on') === 0){ _event = event.replace('on', ''); if(object[event] === null) object[event] = EventsRegistry.readyProxyObserver(_event, url, noCall); }else{ if(evts.indexOf(event) == -1) object.addEventListener(event, EventsRegistry.readyProxyObserver(event, url, noCall), false); } }); } }, _disconnect = function(object, url){ var proxy, num_of_connections_closed = 0, urls = [], time = (new Date)*1; if(toString.call(object.constructor) !== _func_obj){ _each(object, function(connection, url){ proxy = EventsRegistry.getProxy(url); if(proxy !== null){ _each(proxy.events, function(event){ urls.push(url); connection.removeEventListener(event, proxy.callback); connection.close(); ++num_of_connections_closed; }); } }); }else{ if(url){ proxy = EventsRegistry.getProxy(url); if(proxy !== null){ urls.push(url); if(proxy.events.indexOf(event) > -1){ object.removeEventListener(event, proxy.callback); } object.close(); ++num_of_connections_closed; } } } object = null; // free memory return function(callback){ if(typeof callback == "function"){ callback({ type:"closed", timestamp:time, urls:urls, closed_connections:num_of_connections_closed }); } } }, _isEmpty = function(obj){ if(typeof obj !== "object"){ return false; } for(var i in obj){ if(hasOWnProp.call(obj, i)){ return false; } } return true; }, _objectToQuery = function(object, _url, _prefix){ var query = _url.indexOf("?") == -1 ? "?" : ""; if(!object || _isEmpty(object)){ return _url; } _each(object, function(qval, qkey){ if(toString.call(qval) === _arr_obj) { for (var i_qval = 0; i_qval < qval.length; i_qval++){ query += (_prefix + win.encodeURIComponent(qkey)) + "=" + win.encodeURIComponent(qval[i_qval]) + "&"; } } else if(typeof qval === 'string') { query += (_prefix + win.encodeURIComponent(qkey)) + "=" + win.encodeURIComponent(qval) + "&"; } }); return _url + (query.length <= 1)? "" : (query.replace(/\&$/, '')); }, _objectToForm = function(object, _prefix){ var form = ""; if(!object || _isEmpty(object)){ return _url; } _each(object, function(qval, qkey){ if(qkey === 'retry'){ qval = String(qval) } if(toString.call(qval) === _arr_obj || toString.call(qval) === _obj_obj) { if(qkey !== 'target') { form += (_prefix + win.encodeURIComponent(qkey)) + "=" + win.encodeURIComponent(JSON.stringify(qval)) + "&"; }else{ var sub = _objectToQuery(qval, "", "") form += sub.replace('?', ''); } } else if(typeof qval === 'string') { form += (_prefix + win.encodeURIComponent(qkey)) + "=" + win.encodeURIComponent(qval) + "&"; } }); return (form.length == 0)? form : (form.replace(/\&$/, '')); }, Clients = function (settings, forceNew){ if(toString.call(settings) !== _obj_obj){ settings = {} } var url = settings.source || ''; var params = settings.params || null; var options = settings.options || null; var connection = null; if(url === ''){ throw new Error('Cannot connect to stream: URL missing'); } if(toString.call(url) === _url_obj){ url = url.toString(); } if(!rgx_url.exec(url)){ url = _origin + url; } if(!_isShim){ url = _objectToQuery(params, url, ""); }else{ options.getArgs = params; } if(String(options.headers) == _obj_obj){ url = _objectToQuery(options.headers, url, ":"); } if(options.crossdomain === true){ options.withCredentials = options.crossdomain; delete options.crossdomain; } //setTimeout(function delayConnection() { if(!hasOWnProp.call(connections, url)){ connection = new EventSource(url, options); }else{ connection = connections[url]; if(forceNew === true){ _temp = _disconnect(connection, url); connection = _temp = null; // free memory connection = new EventSource(url, options); _installEvents(connection, (options === null || !_isEmpty(options))); } } // @TODO check if cyclic referencing exists here later... connections[url] = connection; //}, 0); // enforce new return (this === win)? new Clients(settings, forceNew): this; }; Clients.prototype.disconnect = function(callback){ var interface = _disconnect(connections); connections = {}; interface(callback); return (interface = null); // free memory } Clients.prototype.on = function(compositeHandlerList){ EventsRegistry.addGlobalHandler(compositeHandlerList); _installEvents(connections); } Clients.prototype.off = function(event){ var evt = event || ''; EventsRegistry.removeGlobalHandler(evt); } var _makeClients = function(settings, forceNew){ if(_clientsObject === null){ _clientsObject = new Clients(settings, forceNew || false); } }; function BeamzerClient(settings){ if(!settings){ settings = {}; } this.url = settings.source || ""; if(_instance === null){ // Singleton _instance = { constructor:BeamzerClient, start:function(openCallback, errorCallback, msgCallback){ _makeClients(settings); _clientsObject.on({'open':openCallback,'error':errorCallback,'message':msgCallback}); }, on:function(event, callback){ if(event.match(open_or_error)){ return; } event = event.replace('on', ''); // hoisting if(!!_clientsObject){ _clientsObject.on({event:callback}); }else{ throw new Error("Illegal Invocation: Beamzer connection instance doesn't yet exist. try call `start()` first") } }, off:function(event){ if(!!_clientsObject){ _clientsObject.off(event); }else{ throw new Error("Illegal Invocation: Beamzer connection instance doesn't yet exist. try call `start()` first") } }, newClient:function(settings){ forceNew = (typeof(settings.params) == "object" && !_isEmpty(settings.params)); _makeClients(settings, forceNew); }, stop:function(callback){ if(!!_clientsObject){ _clientsObject.disconnect(callback); }else{ throw new Error("Illegal Invocation: Beamzer connection instance doesn't yet exist. try call `start()` first") } } } } return _instance; }; /** * MERCURE PROTOCOL - Helpers * * These static methods are for use with the mercure protocol * https://www.github.com/dunglas/mercure * * - Hub Discovery/Authorization * * BeamzerClient.serviceDiscoveryAndAuth(['*']).then(function(details){ * * var beam = new BeamzerClient({ * source:details.hubUrl, * params:{ * topic: details.topics.map(function(topic){ return topic['@id']; }) * }, * options:{loggingEnabled:true, crossdomain:true, interval:4500} * }) * * return beam; * }).then(function(beam){ * document.addEventListener('DOMContentLoaded', function(){ * beam.start( * function onOpen(e){ * }, * function onError(e){ * * }, * null * ); * }, false); * * window.addEventListener('beforeunload', function(e){ * beam.stop(function(e){ * navigator.sendBeacon('') * }) * }, false); * }); * * * * * * * * BeamzerClient.publishToHub({ * retry:4000, * topic:'https://app.beamzer.co/example/activity/chat', * data:{ * message:"Hello there...", * sent_by:'user_96435799292', * sent_to:'thread_24237291120' * } * }) * */ BeamzerClient.publicKey = null; BeamzerClient.targets = {}; // targets = {'https://www.example.com':['https://app.beamzer.co/example/default', 'https://app.beamzer.co/example/activity/chat']} BeamzerClient.serviceDiscoveryAndAuth = function(targets){ targets = targets || [ _origin ]; var pathname = '/beamzer/discovery/auth'; var discoveryAuthUrl = _origin + pathname; return new Promise(function (resolve, reject) { var req = ((typeof win.toStaticHTML === 'function') ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest); if(typeof req.setRequestHeader === 'function'){ req.setRequestHeader('Accept', 'application/ld+json'); //req.setRequestHeader('X-Api-Key', String(BeamzerClient.publicKey)); req.setRequestHeader('Cache-Control', 'no-transform'); } req.ontimeout = function(e){ reject(e) }; req.onerror = function(e){ reject(e); }; req.timeout = 8100; req.onreadystatechange = function(e){ var linkHeader = null; var apiKey = null; if ((e.type === "readystatechange" && req.readyState === 4)) { if (req.status === 200 || (req.status >= 300 && req.status < 400)){ targetDetails = JSON.parse(req.responseText); if(typeof req.getResponseHeader === 'function') { linkHeader = req.getResponseHeader('Link') || null; apiKey = req.getResponseHeader('X-Beamzer-Public-Key') || null; } if(linkHeader === null){ linkHeader = '<https://service.beamzer.co/hub>; rel="mercure"'; } BeamzerClient.targets = targetDetails; BeamzerClient.publicKey = apiKey; resolve({ hubUrl:linkHeader.match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1], topics:BeamzerClient.targets[_origin] || [] }) } } }; req.open('GET', _objectToQuery(discoveryAuthUrl, {targets:JSON.stringify(targets)}, ""), true); req.send(null); }); }; BeamzerClient.pingAuthExpiration = function(){ var expUrl = beamzerOrigin + '/hub/auth/expire'; return new Promise(function (resolve, reject) { var req = new Image(); req.onload = function(e){ resolve(true) }; req.onerror = function(e){ reject(false) }; req.src = _objectToQuery({ publickey:String(BeamzerClient.publicKey) }, expUrl, "") }); }; BeamzerClient.publishToHub = function(requestData){ var pathname = '/hub/publish'; var hubUrl = beamzerOrigin + pathname; requestData = requestData || { target: [ _origin ], data:{title:"Activity Stream", body:"A New Text Stream!"}, topic:'default', // https://app.beamzer.co/example/default type:'broadcast', retry:3000, id:'cjld2cjxh0000qzrmn831i7rn' // cuid }; if((BeamzerClient.targets[_origin] || []).indexOf(requestData.topic || "") === -1){ return Promise.reject(new Error('Invalid Entry: topic `'+ requestData.topic +'` supplied is not available for current target')); } return new Promise(function (resolve, reject) { var req = ((typeof win.toStaticHTML === 'function') ? new XDomainRequest : new XMLHttpRequest); if(typeof req.setRequestHeader === 'function'){ req.setRequestHeader('Accept', 'application/ld+json') req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); req.setRequestHeader('X-Api-Key', String(BeamzerClient.publicKey)); }else{ // {XDomainRequest} can't set headers as the API is missing unlike {XMLHttpRequest} req.headers = { 'content-type': 'application/x-www-form-urlencoded', 'accept': 'application/ld+json', 'origin': _origin.replace(/\/$/, ''), 'x-api-key': String(BeamzerClient.publicKey) }; } if(hasOWnProp.call(req.prototype, 'withCredentials')) { req.withCredentials = true; } req.onerror = function(e){ reject(e) } req.ontimeout = function(e){ reject(e) } req.onprogress = function(){} if(typeof win.toStaticHTML === 'function') { req.open('POST', _objectToQuery(req.headers, hubUrl, ''), true); }else{ req.open('POST', hubUrl, true); } req.onreadystatechange = function(e){ if ((e.type === "readystatechange" && req.readyState === 4)) { if (req.status === 200 || req.status === 201){ dispatchDetails = JSON.parse(req.responseText); resolve({ dispatch:dispatchDetails }) } } }; setTimeout(function(){ req.send( _objectToForm(requestData, "") ); }, 0); }); } return BeamzerClient; }));