(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.webstomp = factory()); }(this, (function () { 'use strict'; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; var VERSIONS = { V1_0: '1.0', V1_1: '1.1', V1_2: '1.2', // Versions of STOMP specifications supported supportedVersions: function supportedVersions() { return '1.2,1.1,1.0'; }, supportedProtocols: function supportedProtocols() { return ['v10.stomp', 'v11.stomp', 'v12.stomp']; } }; var PROTOCOLS_VERSIONS = { 'v10.stomp': VERSIONS.V1_0, 'v11.stomp': VERSIONS.V1_1, 'v12.stomp': VERSIONS.V1_2 }; function getSupportedVersion(protocol, debug) { var knownVersion = PROTOCOLS_VERSIONS[protocol]; if (!knownVersion && debug) { debug('DEPRECATED: ' + protocol + ' is not a recognized STOMP version. In next major client version, this will close the connection.'); } // 2nd temporary fallback if the protocol // does not match a supported STOMP version // This fallback will be removed in next major version return knownVersion || VERSIONS.V1_2; } // Define constants for bytes used throughout the code. var BYTES = { // LINEFEED byte (octet 10) LF: '\x0A', // NULL byte (octet 0) NULL: '\x00' }; // utility function to trim any whitespace before and after a string var trim = function trim(str) { return str.replace(/^\s+|\s+$/g, ''); }; // from https://coolaj86.com/articles/unicode-string-to-a-utf-8-typed-array-buffer-in-javascript/ function unicodeStringToTypedArray(s) { var escstr = encodeURIComponent(s); var binstr = escstr.replace(/%([0-9A-F]{2})/g, function (match, p1) { return String.fromCharCode('0x' + p1); }); var arr = Array.prototype.map.call(binstr, function (c) { return c.charCodeAt(0); }); return new Uint8Array(arr); } // from https://coolaj86.com/articles/unicode-string-to-a-utf-8-typed-array-buffer-in-javascript/ function typedArrayToUnicodeString(ua) { var binstr = String.fromCharCode.apply(String, toConsumableArray(ua)); var escstr = binstr.replace(/(.)/g, function (m, p) { var code = p.charCodeAt(0).toString(16).toUpperCase(); if (code.length < 2) { code = '0' + code; } return '%' + code; }); return decodeURIComponent(escstr); } // Compute the size of a UTF-8 string by counting its number of bytes // (and not the number of characters composing the string) function sizeOfUTF8(s) { if (!s) return 0; return encodeURIComponent(s).match(/%..|./g).length; } function createId() { var ts = new Date().getTime(); var rand = Math.floor(Math.random() * 1000); return ts + '-' + rand; } // [STOMP Frame](http://stomp.github.com/stomp-specification-1.1.html#STOMP_Frames) Class var Frame = function () { // Frame constructor function Frame(command) { var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var body = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; classCallCheck(this, Frame); this.command = command; this.headers = headers; this.body = body; } // Provides a textual representation of the frame // suitable to be sent to the server createClass(Frame, [{ key: 'toString', value: function toString() { var _this = this; var lines = [this.command], skipContentLength = this.headers['content-length'] === false; if (skipContentLength) delete this.headers['content-length']; Object.keys(this.headers).forEach(function (name) { var value = _this.headers[name]; lines.push(name + ':' + value); }); if (this.body && !skipContentLength) { lines.push('content-length:' + sizeOfUTF8(this.body)); } lines.push(BYTES.LF + this.body); return lines.join(BYTES.LF); } // Unmarshall a single STOMP frame from a `data` string }], [{ key: 'unmarshallSingle', value: function unmarshallSingle(data) { // search for 2 consecutives LF byte to split the command // and headers from the body var divider = data.search(new RegExp(BYTES.LF + BYTES.LF)), headerLines = data.substring(0, divider).split(BYTES.LF), command = headerLines.shift(), headers = {}, body = '', // skip the 2 LF bytes that divides the headers from the body bodyIndex = divider + 2; // Parse headers in reverse order so that for repeated headers, the 1st // value is used var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = headerLines.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var line = _step.value; var idx = line.indexOf(':'); headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); } // Parse body // check for content-length or topping at the first NULL byte found. } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } if (headers['content-length']) { var len = parseInt(headers['content-length'], 10); body = ('' + data).substring(bodyIndex, bodyIndex + len); } else { var chr = null; for (var i = bodyIndex; i < data.length; i++) { chr = data.charAt(i); if (chr === BYTES.NULL) break; body += chr; } } return new Frame(command, headers, body); } // Split the data before unmarshalling every single STOMP frame. // Web socket servers can send multiple frames in a single websocket message. // If the message size exceeds the websocket message size, then a single // frame can be fragmented across multiple messages. // // `datas` is a string. // // returns an *array* of Frame objects }, { key: 'unmarshall', value: function unmarshall(datas) { // split and unmarshall *multiple STOMP frames* contained in a *single WebSocket frame*. // The data is split when a NULL byte (followed by zero or many LF bytes) is found var frames = datas.split(new RegExp(BYTES.NULL + BYTES.LF + '*')), firstFrames = frames.slice(0, -1), lastFrame = frames.slice(-1)[0], r = { frames: firstFrames.map(function (f) { return Frame.unmarshallSingle(f); }), partial: '' }; // If this contains a final full message or just a acknowledgement of a PING // without any other content, process this frame, otherwise return the // contents of the buffer to the caller. if (lastFrame === BYTES.LF || lastFrame.search(RegExp(BYTES.NULL + BYTES.LF + '*$')) !== -1) { r.frames.push(Frame.unmarshallSingle(lastFrame)); } else { r.partial = lastFrame; } return r; } // Marshall a Stomp frame }, { key: 'marshall', value: function marshall(command, headers, body) { var frame = new Frame(command, headers, body); return frame.toString() + BYTES.NULL; } }]); return Frame; }(); // STOMP Client Class // // All STOMP protocol is exposed as methods of this class (`connect()`, // `send()`, etc.) var Client = function () { function Client(ws) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; classCallCheck(this, Client); // cannot have default options object + destructuring in the same time in method signature var _options$binary = options.binary, binary = _options$binary === undefined ? false : _options$binary, _options$heartbeat = options.heartbeat, heartbeat = _options$heartbeat === undefined ? { outgoing: 10000, incoming: 10000 } : _options$heartbeat, _options$debug = options.debug, debug = _options$debug === undefined ? true : _options$debug, _options$protocols = options.protocols, protocols = _options$protocols === undefined ? [] : _options$protocols; this.ws = ws; this.ws.binaryType = 'arraybuffer'; this.isBinary = !!binary; this.hasDebug = !!debug; this.connected = false; // Heartbeat properties of the client // outgoing: send heartbeat every 10s by default (value is in ms) // incoming: expect to receive server heartbeat at least every 10s by default // falsy value means no heartbeat hence 0,0 this.heartbeat = heartbeat || { outgoing: 0, incoming: 0 }; // maximum *WebSocket* frame size sent by the client. If the STOMP frame // is bigger than this value, the STOMP frame will be sent using multiple // WebSocket frames (default is 16KiB) this.maxWebSocketFrameSize = 16 * 1024; // subscription callbacks indexed by subscriber's ID this.subscriptions = {}; this.partialData = ''; this.protocols = protocols; } // //// Debugging // // By default, debug messages are logged in the window's console if it is defined. // This method is called for every actual transmission of the STOMP frames over the // WebSocket. // // It is possible to set a `debug(message, data)` method // on a client instance to handle differently the debug messages: // // client.debug = function(str) { // // append the debug log to a #debug div // $("#debug").append(str + "\n"); // }; createClass(Client, [{ key: 'debug', value: function debug() { var _console; if (this.hasDebug) (_console = console).log.apply(_console, arguments); } // [CONNECT Frame](http://stomp.github.com/stomp-specification-1.1.html#CONNECT_or_STOMP_Frame) // // The `connect` method accepts different number of arguments and types: // // * `connect(headers, connectCallback)` // * `connect(headers, connectCallback, errorCallback)` // * `connect(login, passcode, connectCallback)` // * `connect(login, passcode, connectCallback, errorCallback)` // * `connect(login, passcode, connectCallback, errorCallback, host)` // // The errorCallback is optional and the 2 first forms allow to pass other // headers in addition to `client`, `passcode` and `host`. }, { key: 'connect', value: function connect() { var _this = this; var _parseConnect2 = this._parseConnect.apply(this, arguments), _parseConnect3 = slicedToArray(_parseConnect2, 3), headers = _parseConnect3[0], connectCallback = _parseConnect3[1], errorCallback = _parseConnect3[2]; this.connectCallback = connectCallback; this.debug('Opening Web Socket...'); this.ws.onmessage = function (evt) { var data = evt.data; if (evt.data instanceof ArrayBuffer) { data = typedArrayToUnicodeString(new Uint8Array(evt.data)); } _this.serverActivity = Date.now(); // heartbeat if (data === BYTES.LF) { _this.debug('<<< PONG'); return; } _this.debug('<<< ' + data); // Handle STOMP frames received from the server // The unmarshall function returns the frames parsed and any remaining // data from partial frames. var unmarshalledData = Frame.unmarshall(_this.partialData + data); _this.partialData = unmarshalledData.partial; unmarshalledData.frames.forEach(function (frame) { switch (frame.command) { // [CONNECTED Frame](http://stomp.github.com/stomp-specification-1.1.html#CONNECTED_Frame) case 'CONNECTED': _this.debug('connected to server ' + frame.headers.server); _this.connected = true; _this.version = frame.headers.version; _this._setupHeartbeat(frame.headers); if (connectCallback) connectCallback(frame); break; // [MESSAGE Frame](http://stomp.github.com/stomp-specification-1.1.html#MESSAGE) case 'MESSAGE': // the `onreceive` callback is registered when the client calls // `subscribe()`. // If there is registered subscription for the received message, // we used the default `onreceive` method that the client can set. // This is useful for subscriptions that are automatically created // on the browser side (e.g. [RabbitMQ's temporary // queues](http://www.rabbitmq.com/stomp.html)). var subscription = frame.headers.subscription; var onreceive = _this.subscriptions[subscription] || _this.onreceive; if (onreceive) { // 1.2 define ack header if ack is set to client // and this header must be used for ack/nack var messageID = _this.version === VERSIONS.V1_2 && frame.headers.ack || frame.headers['message-id']; // add `ack()` and `nack()` methods directly to the returned frame // so that a simple call to `message.ack()` can acknowledge the message. frame.ack = _this.ack.bind(_this, messageID, subscription); frame.nack = _this.nack.bind(_this, messageID, subscription); onreceive(frame); } else { _this.debug('Unhandled received MESSAGE: ' + frame); } break; // [RECEIPT Frame](http://stomp.github.com/stomp-specification-1.1.html#RECEIPT) // // The client instance can set its `onreceipt` field to a function taking // a frame argument that will be called when a receipt is received from // the server: // // client.onreceipt = function(frame) { // receiptID = frame.headers['receipt-id']; // ... // } case 'RECEIPT': if (_this.onreceipt) _this.onreceipt(frame); break; // [ERROR Frame](http://stomp.github.com/stomp-specification-1.1.html#ERROR) case 'ERROR': if (errorCallback) errorCallback(frame); break; default: _this.debug('Unhandled frame: ' + frame); } }); }; this.ws.onclose = function (event) { _this.debug('Whoops! Lost connection to ' + _this.ws.url + ':', { event: event }); _this._cleanUp(); if (errorCallback) errorCallback(event); }; this.ws.onopen = function () { _this.debug('Web Socket Opened...'); // 1st protocol fallback on user 1st protocols options // to prevent edge case where server does not comply and respond with a choosen protocol // or when ws client does not handle protocol property very well headers['accept-version'] = getSupportedVersion(_this.ws.protocol || _this.protocols[0], _this.debug.bind(_this)); // Check if we already have heart-beat in headers before adding them if (!headers['heart-beat']) { headers['heart-beat'] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(','); } _this._transmit('CONNECT', headers); }; if (this.ws.readyState === this.ws.OPEN) { this.ws.onopen(); } } // [DISCONNECT Frame](http://stomp.github.com/stomp-specification-1.1.html#DISCONNECT) }, { key: 'disconnect', value: function disconnect(disconnectCallback) { var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this._transmit('DISCONNECT', headers); // Discard the onclose callback to avoid calling the errorCallback when // the client is properly disconnected. this.ws.onclose = null; this.ws.close(); this._cleanUp(); // TODO: what's the point of this callback disconnect is not async if (disconnectCallback) disconnectCallback(); } // [SEND Frame](http://stomp.github.com/stomp-specification-1.1.html#SEND) // // * `destination` is MANDATORY. }, { key: 'send', value: function send(destination) { var body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var hdrs = Object.assign({}, headers); hdrs.destination = destination; this._transmit('SEND', hdrs, body); } // [BEGIN Frame](http://stomp.github.com/stomp-specification-1.1.html#BEGIN) // // If no transaction ID is passed, one will be created automatically }, { key: 'begin', value: function begin() { var transaction = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'tx-' + createId(); this._transmit('BEGIN', { transaction: transaction }); return { id: transaction, commit: this.commit.bind(this, transaction), abort: this.abort.bind(this, transaction) }; } // [COMMIT Frame](http://stomp.github.com/stomp-specification-1.1.html#COMMIT) // // * `transaction` is MANDATORY. // // It is preferable to commit a transaction by calling `commit()` directly on // the object returned by `client.begin()`: // // var tx = client.begin(txid); // ... // tx.commit(); }, { key: 'commit', value: function commit(transaction) { this._transmit('COMMIT', { transaction: transaction }); } // [ABORT Frame](http://stomp.github.com/stomp-specification-1.1.html#ABORT) // // * `transaction` is MANDATORY. // // It is preferable to abort a transaction by calling `abort()` directly on // the object returned by `client.begin()`: // // var tx = client.begin(txid); // ... // tx.abort(); }, { key: 'abort', value: function abort(transaction) { this._transmit('ABORT', { transaction: transaction }); } // [ACK Frame](http://stomp.github.com/stomp-specification-1.1.html#ACK) // // * `messageID` & `subscription` are MANDATORY. // // It is preferable to acknowledge a message by calling `ack()` directly // on the message handled by a subscription callback: // // client.subscribe(destination, // function(message) { // // process the message // // acknowledge it // message.ack(); // }, // {'ack': 'client'} // ); }, { key: 'ack', value: function ack(messageID, subscription) { var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var hdrs = Object.assign({}, headers); // 1.2 change id header name from message-id to id var idAttr = this.version === VERSIONS.V1_2 ? 'id' : 'message-id'; hdrs[idAttr] = messageID; hdrs.subscription = subscription; this._transmit('ACK', hdrs); } // [NACK Frame](http://stomp.github.com/stomp-specification-1.1.html#NACK) // // * `messageID` & `subscription` are MANDATORY. // // It is preferable to nack a message by calling `nack()` directly on the // message handled by a subscription callback: // // client.subscribe(destination, // function(message) { // // process the message // // an error occurs, nack it // message.nack(); // }, // {'ack': 'client'} // ); }, { key: 'nack', value: function nack(messageID, subscription) { var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var hdrs = Object.assign({}, headers); // 1.2 change id header name from message-id to id var idAttr = this.version === VERSIONS.V1_2 ? 'id' : 'message-id'; hdrs[idAttr] = messageID; hdrs.subscription = subscription; this._transmit('NACK', hdrs); } // [SUBSCRIBE Frame](http://stomp.github.com/stomp-specification-1.1.html#SUBSCRIBE) }, { key: 'subscribe', value: function subscribe(destination, callback) { var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var hdrs = Object.assign({}, headers); // for convenience if the `id` header is not set, we create a new one for this client // that will be returned to be able to unsubscribe this subscription if (!hdrs.id) hdrs.id = 'sub-' + createId(); hdrs.destination = destination; this.subscriptions[hdrs.id] = callback; this._transmit('SUBSCRIBE', hdrs); return { id: hdrs.id, unsubscribe: this.unsubscribe.bind(this, hdrs.id) }; } // [UNSUBSCRIBE Frame](http://stomp.github.com/stomp-specification-1.1.html#UNSUBSCRIBE) // // * `id` is MANDATORY. // // It is preferable to unsubscribe from a subscription by calling // `unsubscribe()` directly on the object returned by `client.subscribe()`: // // var subscription = client.subscribe(destination, onmessage); // ... // subscription.unsubscribe(headers); }, { key: 'unsubscribe', value: function unsubscribe(id) { var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var hdrs = Object.assign({}, headers); delete this.subscriptions[id]; hdrs.id = id; this._transmit('UNSUBSCRIBE', hdrs); } // Clean up client resources when it is disconnected or the server did not // send heart beats in a timely fashion }, { key: '_cleanUp', value: function _cleanUp() { this.connected = false; clearInterval(this.pinger); clearInterval(this.ponger); } // Base method to transmit any stomp frame }, { key: '_transmit', value: function _transmit(command, headers, body) { var out = Frame.marshall(command, headers, body); this.debug('>>> ' + out, { frame: { command: command, headers: headers, body: body } }); this._wsSend(out); } }, { key: '_wsSend', value: function _wsSend(data) { if (this.isBinary) data = unicodeStringToTypedArray(data); this.debug('>>> length ' + data.length); // if necessary, split the *STOMP* frame to send it on many smaller // *WebSocket* frames while (true) { if (data.length > this.maxWebSocketFrameSize) { this.ws.send(data.slice(0, this.maxWebSocketFrameSize)); data = data.slice(this.maxWebSocketFrameSize); this.debug('remaining = ' + data.length); } else { return this.ws.send(data); } } } // Heart-beat negotiation }, { key: '_setupHeartbeat', value: function _setupHeartbeat(headers) { var _this2 = this; if (this.version !== VERSIONS.V1_1 && this.version !== VERSIONS.V1_2) return; // heart-beat header received from the server looks like: // // heart-beat: sx, sy var _split$map = (headers['heart-beat'] || '0,0').split(',').map(function (v) { return parseInt(v, 10); }), _split$map2 = slicedToArray(_split$map, 2), serverOutgoing = _split$map2[0], serverIncoming = _split$map2[1]; if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) { var ttl = Math.max(this.heartbeat.outgoing, serverIncoming); this.debug('send PING every ' + ttl + 'ms'); this.pinger = setInterval(function () { _this2._wsSend(BYTES.LF); _this2.debug('>>> PING'); }, ttl); } if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) { var _ttl = Math.max(this.heartbeat.incoming, serverOutgoing); this.debug('check PONG every ' + _ttl + 'ms'); this.ponger = setInterval(function () { var delta = Date.now() - _this2.serverActivity; // We wait twice the TTL to be flexible on window's setInterval calls if (delta > _ttl * 2) { _this2.debug('did not receive server activity for the last ' + delta + 'ms'); _this2.ws.close(); } }, _ttl); } } // parse the arguments number and type to find the headers, connectCallback and // (eventually undefined) errorCallback }, { key: '_parseConnect', value: function _parseConnect() { var headers = {}, connectCallback = void 0, errorCallback = void 0; for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } switch (args.length) { case 2: headers = args[0]; connectCallback = args[1]; break; case 3: if (args[1] instanceof Function) { headers = args[0]; connectCallback = args[1]; errorCallback = args[2]; } else { headers.login = args[0]; headers.passcode = args[1]; connectCallback = args[2]; } break; case 4: headers.login = args[0]; headers.passcode = args[1]; connectCallback = args[2]; errorCallback = args[3]; break; default: headers.login = args[0]; headers.passcode = args[1]; connectCallback = args[2]; errorCallback = args[3]; headers.host = args[4]; } return [headers, connectCallback, errorCallback]; } }]); return Client; }(); // The `webstomp` Object var webstomp = { Frame: Frame, VERSIONS: VERSIONS, // This method creates a WebSocket client that is connected to // the STOMP server located at the url. client: function client(url) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var ws = new WebSocket(url, options.protocols || VERSIONS.supportedProtocols()); return new Client(ws, options); }, // This method is an alternative to `webstomp.client()` to let the user // specify the WebSocket to use (either a standard HTML5 WebSocket or // a similar object). over: function over() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return new (Function.prototype.bind.apply(Client, [null].concat(args)))(); } }; return webstomp; })));