diff --git a/.gitignore b/.gitignore index 91fa8cf..d540c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -/node_modules/ +bower_components/ +node_modules/ npm-debug.log + diff --git a/.travis.yml b/.travis.yml index 2cdacaf..b146b5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,10 @@ language: node_js node_js: + - '0.11' - '0.10' - - '0.8' +before_script: + - npm install -g bower + - bower install + - export CHROME_BIN=chromium-browser + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 3c68dbc..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -module.exports = function (grunt) { - - require('load-grunt-tasks')(grunt); - require('time-grunt')(grunt); - - // Project configuration. - grunt.initConfig({ - nodeunit: { - files: ['test/**/*_test.js'] - }, - jshint: { - options: { - jshintrc: '.jshintrc' - }, - gruntfile: { - src: 'Gruntfile.js' - }, - lib: { - src: ['angular-websocket.js'] - }, - test: { - src: ['test/**/*.js'] - } - }, - watch: { - gruntfile: { - files: '<%= jshint.gruntfile.src %>', - tasks: ['jshint:gruntfile'] - }, - lib: { - files: '<%= jshint.lib.src %>', - tasks: ['jshint:lib', 'nodeunit'] - }, - test: { - files: '<%= jshint.test.src %>', - tasks: ['jshint:test', 'nodeunit'] - } - } - }); - - // Default task. - grunt.registerTask('default', [ - 'jshint', - 'nodeunit' - ]); - - grunt.registerTask('test', [ - 'jshint', - 'nodeunit' - ]); - -}; diff --git a/README.md b/README.md index ec00a67..d60286a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ -# angular-websocket [![Build Status](https://travis-ci.org/gdi2290/angular-websocket.png?branch=master)](https://travis-ci.org/gdi2290/angular-websocket) +# angular-websocket +[![Travis](https://img.shields.io/travis/gdi2290/angular-websocket.svg?style=flat)](https://travis-ci.org/gdi2290/angular-websocket) +[![Bower](https://img.shields.io/bower/v/angular-websocket.svg?style=flat)](https://github.com/gdi2290/angular-websocket) +[![npm](https://img.shields.io/npm/v/angular-websocket.svg?style=flat)](https://www.npmjs.com/package/angular-websocket) +[![Dependency Status](https://david-dm.org/gdi2290/angular-websocket.svg)](https://david-dm.org/gdi2290/angular-websocket) +[![devDependency Status](https://david-dm.org/gdi2290/angular-websocket/dev-status.svg)](https://david-dm.org/gdi2290/angular-websocket#info=devDependencies) -WebSockets for Angular.js -
-Email me if something is broken. +### Status: Looking for feedback about new API changes -#How do I add this to my project? +An AngularJS 1.x WebSocket service for connecting client applications to servers. + +## How do I add this to my project? You can download angular-websocket by: @@ -12,36 +17,133 @@ You can download angular-websocket by: * Using npm and running `npm install angular-websocket --save` * Downloading it manually by clicking [here to download development unminified version](https://raw.github.com/gdi2290/angular-websocket/master/angular-websocket.js) +## Usage -````html - - - + + + +
+ +
+ + +``` + +## API + +### Factory: `$websocket` (in module `ngWebSocket`) + +returns instance of $Websocket + +### Methods + +name | arguments | description +------------|--------------------------------------------------------|------------ +$websocket
_constructor_ | url:String | Creates and opens a [WebSocket](http://mdn.io/API/WebSocket) instance.
`var ws = $websocket('ws://foo');` +send | data:String,Object returns | Adds data to a queue, and attempts to send if socket is ready. Accepts string or object, and will stringify objects before sending to socket. +onMessage | callback:Function
options{filter:String,RegExp, autoApply:Boolean=true} | Register a callback to be fired on every message received from the websocket, or optionally just when the message's `data` property matches the filter provided in the options object. Each message handled will safely call `$rootScope.$digest()` unless `autoApply` is set to `false in the options. Callback gets called with a [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent?redirectlocale=en-US&redirectslug=WebSockets%2FWebSockets_reference%2FMessageEvent) object. +onOpen | callback:Function | Function to be executed each time a socket connection is opened for this instance. +onClose | callback:Function | Function to be executed each time a socket connection is closed for this instance. +onError | callback:Function | Function to be executed each time a socket connection has an Error for this instance. +close | force:Boolean:_optional_ | Close the underlying socket, as long as no data is still being sent from the client. Optionally force close, even if data is still being sent, by passing `true` as the `force` parameter. To check if data is being sent, read the value of `socket.bufferedAmount`. + +### Properties +name | type | description +-------------------|------------------|------------ +socket | window.WebSocket | [WebSocket](http://mdn.io/API/WebSocket) instance. +sendQueue | Array | Queue of `send` calls to be made on socket when socket is able to receive data. List is populated by calls to the `send` method, but this array can be spliced if data needs to be manually removed before it's been sent to a socket. Data is removed from the array after it's been sent to the socket. +onOpenCallbacks | Array | List of callbacks to be executed when the socket is opened, initially or on re-connection after broken connection. Callbacks should be added to this list through the `onOpen` method. +onMessageCallbacks | Array | List of callbacks to be executed when a message is received from the socket. Callbacks should be added via the `onMessage` method. +readyState | Number:readonly | Returns either the readyState value from the underlying WebSocket instance, or a proprietary value representing the internal state of the lib, e.g. if the lib is in a state of re-connecting. +initialTimeout | Number | The initial timeout, should be set at the outer limits of expected response time for the service. For example, if your service responds in 1ms on average but in 10ms for 99% of requests, then set to 10ms. +maxTimeout | Number | Should be as low as possible to keep your customers happy, but high enough that the system can definitely handle requests from all clients at that sustained rate. + +### CancelablePromise + +This type is returned from the `send()` instance method of $websocket, inherits from [$q.defer().promise](https://ng-click.com/$q). + +### Methods + +name | arguments | description +------------|--------------------------------------------------------|------------ +cancel | | Alias to `deferred.reject()`, allows preventing an unsent message from being sent to socket for any arbitrary reason. +then | resolve:Function, reject:Function | Resolves when message has been passed to socket, presuming the socket has a `readyState` of 1. Rejects if the socket is hopelessly disconnected now or in the future (i.e. the library is no longer attempting to reconnect). All messages are immediately rejected when the library has determined that re-establishing a connection is unlikely. + + +### Service: `$websocketBackend` (in module `ngWebSocketMock`) + +Similar to [`httpBackend`](https://ng-click.com/$httpBackend) mock in AngularJS's `ngMock` module + +### Methods + +name | arguments | description +-------------------------------|------------|----------------------------------- +flush | | Executes all pending requests +expectConnect | url:String | Specify the url of an expected WebSocket connection +expectClose | | Expect "close" to be called on the WebSocket +expectSend | msg:String | Expectation of send to be called, with required message +verifyNoOutstandingExpectation | | Makes sure all expectations have been satisfied, should be called in afterEach +verifyNoOutstandingRequest | | Makes sure no requests are pending, should be called in afterEach + +## Logical Questions + + * *Q.*: What if the browser doesn't support WebSockets? + * *A.*: This module will not help; it does not have a fallback story for browsers that do not support WebSockets. Please check your browser target support [here](http://caniuse.com/#feat=websockets) + +## Development + +```shell +$ npm install +$ bower install +``` + +### Unit Tests +`$ npm test` Run karma in Chrome, Firefox, and Safari + +### Manual Tests + +In the project root directory open `index.html` in the example folder + +### Distribute +`$ npm run dist` Builds files with uglifyjs + + +## TODO + * Allow JSON if object is sent + * Allow more control over $digest cycle per WebSocket instance + +## License +[MIT](https://github.com/gdi2290/angular-websocket/blob/master/LICENSE) -```` diff --git a/angular-websocket.js b/angular-websocket.js deleted file mode 100644 index 3aff82f..0000000 --- a/angular-websocket.js +++ /dev/null @@ -1,122 +0,0 @@ -;(function(module, undefined) { -'use strict'; - -module.provider('WebSocket', function() { - // when forwarding events, prefix the event name - var _prefix = 'websocket:'; - var _WebSocket; - var _uri; - var _protocols; - var _definedEvents = []; - - this.prefix = function(newPrefix) { - _prefix = newPrefix; - return this; - }; - - this.uri = function(uri, protocols) { - protocols = Array.prototype.slice.call(arguments, 1); - _uri = uri; - _protocols = protocols; - _WebSocket = new WebSocket(uri, protocols); - return this; - }; - - // expose to provider - this.$get = ['$rootScope', '$timeout', function($rootScope, $timeout) { - - var ws = _WebSocket; - - var asyncAngularify = function (callback) { - return function(args) { - args = Array.prototype.slice.call(arguments); - $timeout(function() { - callback.apply(ws, args); - }); - }; - }; - - var addListener = function(event) { - event = event && 'on'+event || 'onmessage'; - return function(callback) { - ws[event] = asyncAngularify(callback); - _definedEvents.push(event); - return this; - }; - }; - - var wrappedWebSocket = { - states: ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'], - on: function(event, callback) { - return addListener(event)(callback); - }, - onmessage: addListener('message'), - onclose: addListener('close'), - onopen: addListener('open'), - onerror: addListener('error'), - new: function() { - var oldws = ws; - ws = new WebSocket(_uri, _protocols); - //assign the old events to the new websocket - var _len; - for (var i = 0, _len = _definedEvents.length; i < _len; i++) { - ws[_definedEvents[i]] = oldws[_definedEvents[i]]; - } - return this; - }, - close: function() { - ws.close(); - return this - }, - readyState: function() { - return ws.readyState - }, - currentState: function() { - return this.states[ws.readyState]; - }, - send: function(message) { - message = Array.prototype.slice.call(arguments); - ws.send.apply(ws, message); - return this; - }, - - removeListener: function(args) { - args = Array.prototype.slice.call(arguments); - ws.removeEventListener.apply(ws, args); - return this; - }, - - // when ws.on('someEvent', fn (data) { ... }), - // call scope.$broadcast('someEvent', data) - forward: function(events, scope) { - - if (events instanceof Array === false) { - events = [events]; - } - - if (!scope) { - scope = $rootScope; - } - - events.forEach(function(eventName) { - var prefixedEvent = _prefix + eventName; - var forwardEvent = asyncAngularify(function(data) { - scope.$broadcast(prefixedEvent, data); - }); - scope.$on('$destroy', function () { - ws.removeEventListener(eventName, forwardEvent); - }); - ws.onmessage(eventName, forwardEvent); - }); - return this; - - } - }; - - return wrappedWebSocket; - - }]; - -}); - -}(angular.module('angular-websocket', []))); diff --git a/bower.json b/bower.json index c26ae1c..2940d2d 100644 --- a/bower.json +++ b/bower.json @@ -1,11 +1,11 @@ { "name": "angular-websocket", - "version": "0.0.6", + "version": "1.0.0", "homepage": "https://github.com/gdi2290/angular-websocket", "authors": [ "gdi2290 " ], - "main": "angular-websocket.js", + "main": "dist/angular-websocket.min.js", "description": "WebSocket service for Angular.js", "ignore": [ "**/.*", @@ -18,5 +18,9 @@ ], "dependencies": { "angular": "*" + }, + "devDependencies": { + "angular": "~1.3.8", + "angular-mocks": "~1.3.8" } } diff --git a/dist.sh b/dist.sh new file mode 100755 index 0000000..d6436aa --- /dev/null +++ b/dist.sh @@ -0,0 +1,24 @@ +#!/bin/sh +mkdir -p dist/ + +echo "* Copy files" +cat src/angular-websocket.js > dist/angular-websocket.js +cat src/angular-websocket-mock.js > dist/angular-websocket-mock.js + +echo "dist/angular-websocket.js" +echo "dist/angular-websocket-mock.js" +echo + +echo "* Build source files" + +./node_modules/.bin/uglifyjs src/angular-websocket.js > dist/angular-websocket.min.js \ +--source-map dist/angular-websocket.min.js.map \ +--source-map-url angular-websocket.min.js.map \ +--mangle \ +--compress \ +--stats + +echo +echo "dist/angular-websocket.min.js" +echo "dist/angular-websocket.min.js.map" +echo diff --git a/dist/angular-websocket-mock.js b/dist/angular-websocket-mock.js new file mode 100644 index 0000000..1c426fb --- /dev/null +++ b/dist/angular-websocket-mock.js @@ -0,0 +1,102 @@ +(function() { + + function $WebSocketBackend() { + var connectQueue = []; + var pendingConnects = []; + var closeQueue = []; + var pendingCloses = []; + var sendQueue = []; + var pendingSends = []; + + + function $MockWebSocket(url, protocols) { + this.protocols = protocols || 'Sec-WebSocket-Protocol'; + this.ssl = /(wss)/i.test(this.url); + + } + + $MockWebSocket.prototype.send = function (msg) { + pendingSends.push(msg); + }; + + $MockWebSocket.prototype.close = function () { + pendingCloses.push(true); + }; + + + this.createWebSocketBackend = function (url, protocols) { + pendingConnects.push(url); + // pendingConnects.push({ + // url: url, + // protocols: protocols + // }); + + if (protocols) { + return new $MockWebSocket(url, protocols); + } + return new $MockWebSocket(url); + }; + + this.flush = function () { + var url, msg, config; + while (url = pendingConnects.shift()) { + var i = connectQueue.indexOf(url); + if (i > -1) { + connectQueue.splice(i, 1); + } + // if (config && config.url) { + // } + } + + while (pendingCloses.shift()) { + closeQueue.shift(); + } + + while (msg = pendingSends.shift()) { + var j; + sendQueue.forEach(function(pending, i) { + if (pending.message === msg.message) { + j = i; + } + }); + + if (j > -1) { + sendQueue.splice(j, 1); + } + } + }; + + this.expectConnect = function (url, protocols) { + connectQueue.push(url); + // connectQueue.push({url: url, protocols: protocols}); + }; + + this.expectClose = function () { + closeQueue.push(true); + }; + + this.expectSend = function (msg) { + sendQueue.push(msg); + }; + + this.verifyNoOutstandingExpectation = function () { + if (connectQueue.length || closeQueue.length || sendQueue.length) { + throw new Error('Requests waiting to be flushed'); + } + }; + + this.verifyNoOutstandingRequest = function () { + if (pendingConnects.length || pendingCloses.length || pendingSends.length) { + throw new Error('Requests waiting to be processed'); + } + }; + + } // end $WebSocketBackend + + angular.module('ngWebSocketMock', []) + .service('WebSocketBackend', $WebSocketBackend) + .service('$websocketBackend', $WebSocketBackend); + + angular.module('angular-websocket-mock', ['ngWebSocketMock']); + +}()); diff --git a/dist/angular-websocket.js b/dist/angular-websocket.js new file mode 100644 index 0000000..4c7ddc3 --- /dev/null +++ b/dist/angular-websocket.js @@ -0,0 +1,324 @@ +(function() { + + var noop = function() {}; + var objectFreeze = (Object.freeze) ? Object.freeze : noop; + var objectDefineProperty = Object.defineProperty; + var isString = angular.isString; + var isFunction = angular.isFunction; + var isDefined = angular.isDefined; + var isArray = angular.isArray; + // ie8 wat + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(elt /*, from*/) { + var len = this.length >>> 0; + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && this[from] === elt) { return from; } + } + return -1; + }; + } + + $WebSocketProvider.$inject = ['$rootScope', '$q', '$timeout', '$websocketBackend']; + function $WebSocketProvider($rootScope, $q, $timeout, $websocketBackend) { + + function safeDigest(autoApply) { + if (autoApply && !$rootScope.$$phase) { + $rootScope.$apply(); + } + } + + function $WebSocket(url, options) { + // var bits = url.split('/'); + + var protocols = options && options.protocols; + if (isString(options) || isArray(options)) { + protocols = options; + } + + this.protocols = protocols || 'Sec-WebSocket-Protocol'; + this.url = url || 'Missing URL'; + this.ssl = /(wss)/i.test(this.url); + + // this.binaryType = ''; + // this.extensions = ''; + // this.bufferedAmount = 0; + // this.trasnmitting = false; + // this.buffer = []; + + this._reconnectAttempts = 0; + this.initialTimeout = 500; // 500ms + this.maxTimeout = 5 * 60 * 1000; // 5 minutes + this.sendQueue = []; + this.onOpenCallbacks = []; + this.onMessageCallbacks = []; + this.onErrorCallbacks = []; + this.onCloseCallbacks = []; + + objectFreeze(this._readyStateConstants); + + if (url) { + this._connect(); + } else { + this._setInternalState(0); + } + } + + $WebSocket.prototype._readyStateConstants = { + 'CONNECTING': 0, + 'OPEN': 1, + 'CLOSING': 2, + 'CLOSED': 3, + 'RECONNECT_ABORTED': 4 + }; + + $WebSocket.prototype._reconnectableStatusCodes = [ + 4000 + ]; + + $WebSocket.prototype.close = function (force) { + if (force || !this.socket.bufferedAmount) { + this.socket.close(); + } + return this; + }; + + $WebSocket.prototype._connect = function (force) { + if (force || !this.socket || this.socket.readyState !== this._readyStateConstants.OPEN) { + this.socket = $websocketBackend.createWebSocketBackend(this.url, this.protocol); + this.socket.onopen = this._onOpenHandler.bind(this); + this.socket.onmessage = this._onMessageHandler.bind(this); + this.socket.onerror = this._onErrorHandler.bind(this); + this.socket.onclose = this._onCloseHandler.bind(this); + } + }; + + $WebSocket.prototype.fireQueue = function () { + while (this.sendQueue.length && this.socket.readyState === this._readyStateConstants.OPEN) { + var data = this.sendQueue.shift(); + + this.socket.send( + isString(data.message) ? data.message : JSON.stringify(data.message) + ); + data.deferred.resolve(); + } + }; + + $WebSocket.prototype.notifyOpenCallbacks = function () { + for (var i = 0; i < this.onOpenCallbacks.length; i++) { + this.onOpenCallbacks[i].call(this); + } + }; + + $WebSocket.prototype.notifyCloseCallbacks = function (event) { + for (var i = 0; i < this.onCloseCallbacks.length; i++) { + this.onCloseCallbacks[i].call(this, event); + } + }; + $WebSocket.prototype.notifyErrorCallbacks = function (event) { + for (var i = 0; i < this.onErrorCallbacks.length; i++) { + this.onErrorCallbacks[i].call(this, event); + } + }; + + $WebSocket.prototype.onOpen = function (cb) { + this.onOpenCallbacks.push(cb); + return this; + }; + + $WebSocket.prototype.onClose = function (cb) { + this.onCloseCallbacks.push(cb); + return this; + }; + + $WebSocket.prototype.onError = function (cb) { + this.onErrorCallbacks.push(cb); + return this; + }; + + + $WebSocket.prototype.onMessage = function (callback, options) { + if (!isFunction(callback)) { + throw new Error('Callback must be a function'); + } + + if (options && isDefined(options.filter) && !isString(options.filter) && !(options.filter instanceof RegExp)) { + throw new Error('Pattern must be a string or regular expression'); + } + + this.onMessageCallbacks.push({ + fn: callback, + pattern: options ? options.filter : undefined, + autoApply: options ? options.autoApply : true + }); + return this; + }; + + $WebSocket.prototype._onOpenHandler = function () { + this._reconnectAttempts = 0; + this.notifyOpenCallbacks(); + this.fireQueue(); + }; + + $WebSocket.prototype._onCloseHandler = function (event) { + this.notifyCloseCallbacks(event); + if (this._reconnectableStatusCodes.indexOf(event.code) > -1) { + this.reconnect(); + } + }; + + $WebSocket.prototype._onErrorHandler = function (event) { + this.notifyErrorCallbacks(event); + }; + + $WebSocket.prototype._onMessageHandler = function (message) { + var pattern; + var socket = this; + var currentCallback; + for (var i = 0; i < socket.onMessageCallbacks.length; i++) { + currentCallback = socket.onMessageCallbacks[i]; + pattern = currentCallback.pattern; + if (pattern) { + if (isString(pattern) && message.data === pattern) { + currentCallback.fn.call(this, message); + safeDigest(currentCallback.autoApply); + } + else if (pattern instanceof RegExp && pattern.exec(message.data)) { + currentCallback.fn.call(this, message); + safeDigest(currentCallback.autoApply); + } + } + else { + currentCallback.fn.call(this, message); + safeDigest(currentCallback.autoApply); + } + } + }; + + $WebSocket.prototype.send = function (data) { + var deferred = $q.defer(); + var socket = this; + var promise = cancelableify(deferred.promise); + + if (socket.readyState === socket._readyStateConstants.RECONNECT_ABORTED) { + deferred.reject('Socket connection has been closed'); + } + else { + this.sendQueue.push({ + message: data, + deferred: deferred + }); + this.fireQueue(); + } + + // Credit goes to @btford + function cancelableify(promise) { + promise.cancel = cancel; + var then = promise.then; + promise.then = function() { + var newPromise = then.apply(this, arguments); + return cancelableify(newPromise); + }; + return promise; + } + + function cancel(reason) { + socket.sendQueue.splice(socket.sendQueue.indexOf(data), 1); + deferred.reject(reason); + return this; + } + + return promise; + }; + + $WebSocket.prototype.reconnect = function () { + this.close(); + $timeout(angular.bind(this, function() { + this._connect(); + }), this._getBackoffDelay(++this._reconnectAttempts), true); + + return this; + }; + // Exponential Backoff Formula by Prof. Douglas Thain + // http://dthain.blogspot.co.uk/2009/02/exponential-backoff-in-distributed.html + $WebSocket.prototype._getBackoffDelay = function(attempt) { + var R = Math.random() + 1; + var T = this.initialTimeout; + var F = 2; + var N = attempt; + var M = this.maxTimeout; + + return Math.floor(Math.min(R * T * Math.pow(F, N), M)); + }; + + $WebSocket.prototype._setInternalState = function(state) { + if (Math.floor(state) !== state || state < 0 || state > 4) { + throw new Error('state must be an integer between 0 and 4, got: ' + state); + } + + // ie8 wat + if (!objectDefineProperty) { + this.readyState = state || this.socket.readyState; + } + this._internalConnectionState = state; + + + angular.forEach(this.sendQueue, function(pending) { + pending.deferred.reject('Message cancelled due to closed socket connection'); + }); + }; + + if (objectDefineProperty) { + objectDefineProperty($WebSocket.prototype, 'readyState', { + get: function() { + return this._internalConnectionState || this.socket.readyState; + }, + set: function() { + throw new Error('The readyState property is read-only'); + } + }); + } + + return function(url, protocols) { + return new $WebSocket(url, protocols); + }; + } + + $WebSocketBackend.$inject = ['$window']; + function $WebSocketBackend($window) { + this.createWebSocketBackend = function (url, protocols) { + var match = /wss?:\/\//.exec(url); + var Socket, ws; + if (!match) { + throw new Error('Invalid url provided'); + } + if (typeof exports === 'object' && require) { + try { + ws = require('ws'); + Socket = (ws.Client || ws.client || ws); + } catch(e) {} + } + Socket = Socket || $window.WebSocket || $window.MozWebSocket; + + if (protocols) { + return new Socket(url, protocols); + } + return new Socket(url); + }; + } + + angular.module('ngWebSocket', []) + .factory('$websocket', $WebSocketProvider) + .factory('WebSocket', $WebSocketProvider) + .service('$websocketBackend', $WebSocketBackend) + .service('WebSocketBackend', $WebSocketBackend); + + + angular.module('angular-websocket', ['ngWebSocket']); + +}()); diff --git a/dist/angular-websocket.min.js b/dist/angular-websocket.min.js new file mode 100644 index 0000000..43315c8 --- /dev/null +++ b/dist/angular-websocket.min.js @@ -0,0 +1,2 @@ +!function(){function t(t,e,n,l){function h(e){e&&!t.$$phase&&t.$apply()}function u(t,e){var n=e&&e.protocols;(r(e)||c(e))&&(n=e),this.protocols=n||"Sec-WebSocket-Protocol",this.url=t||"Missing URL",this.ssl=/(wss)/i.test(this.url),this._reconnectAttempts=0,this.initialTimeout=500,this.maxTimeout=3e5,this.sendQueue=[],this.onOpenCallbacks=[],this.onMessageCallbacks=[],this.onErrorCallbacks=[],this.onCloseCallbacks=[],o(this._readyStateConstants),t?this._connect():this._setInternalState(0)}return u.prototype._readyStateConstants={CONNECTING:0,OPEN:1,CLOSING:2,CLOSED:3,RECONNECT_ABORTED:4},u.prototype._reconnectableStatusCodes=[4e3],u.prototype.close=function(t){return(t||!this.socket.bufferedAmount)&&this.socket.close(),this},u.prototype._connect=function(t){(t||!this.socket||this.socket.readyState!==this._readyStateConstants.OPEN)&&(this.socket=l.createWebSocketBackend(this.url,this.protocol),this.socket.onopen=this._onOpenHandler.bind(this),this.socket.onmessage=this._onMessageHandler.bind(this),this.socket.onerror=this._onErrorHandler.bind(this),this.socket.onclose=this._onCloseHandler.bind(this))},u.prototype.fireQueue=function(){for(;this.sendQueue.length&&this.socket.readyState===this._readyStateConstants.OPEN;){var t=this.sendQueue.shift();this.socket.send(r(t.message)?t.message:JSON.stringify(t.message)),t.deferred.resolve()}},u.prototype.notifyOpenCallbacks=function(){for(var t=0;t-1&&this.reconnect()},u.prototype._onErrorHandler=function(t){this.notifyErrorCallbacks(t)},u.prototype._onMessageHandler=function(t){for(var e,n,o=this,s=0;st||t>4)throw new Error("state must be an integer between 0 and 4, got: "+t);s||(this.readyState=t||this.socket.readyState),this._internalConnectionState=t,angular.forEach(this.sendQueue,function(t){t.deferred.reject("Message cancelled due to closed socket connection")})},s&&s(u.prototype,"readyState",{get:function(){return this._internalConnectionState||this.socket.readyState},set:function(){throw new Error("The readyState property is read-only")}}),function(t,e){return new u(t,e)}}function e(t){this.createWebSocketBackend=function(e,n){var o,s,r=/wss?:\/\//.exec(e);if(!r)throw new Error("Invalid url provided");if("object"==typeof exports&&require)try{s=require("ws"),o=s.Client||s.client||s}catch(i){}return o=o||t.WebSocket||t.MozWebSocket,n?new o(e,n):new o(e)}}var n=function(){},o=Object.freeze?Object.freeze:n,s=Object.defineProperty,r=angular.isString,i=angular.isFunction,a=angular.isDefined,c=angular.isArray;Array.prototype.indexOf||(Array.prototype.indexOf=function(t){var e=this.length>>>0,n=Number(arguments[1])||0;for(n=0>n?Math.ceil(n):Math.floor(n),0>n&&(n+=e);e>n;n++)if(n in this&&this[n]===t)return n;return-1}),t.$inject=["$rootScope","$q","$timeout","$websocketBackend"],e.$inject=["$window"],angular.module("ngWebSocket",[]).factory("$websocket",t).factory("WebSocket",t).service("$websocketBackend",e).service("WebSocketBackend",e),angular.module("angular-websocket",["ngWebSocket"])}(); +//# sourceMappingURL=angular-websocket.min.js.map \ No newline at end of file diff --git a/dist/angular-websocket.min.js.map b/dist/angular-websocket.min.js.map new file mode 100644 index 0000000..75902e4 --- /dev/null +++ b/dist/angular-websocket.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["src/angular-websocket.js"],"names":["$WebSocketProvider","$rootScope","$q","$timeout","$websocketBackend","safeDigest","autoApply","$$phase","$apply","$WebSocket","url","options","protocols","isString","isArray","this","ssl","test","_reconnectAttempts","initialTimeout","maxTimeout","sendQueue","onOpenCallbacks","onMessageCallbacks","onErrorCallbacks","onCloseCallbacks","objectFreeze","_readyStateConstants","_connect","_setInternalState","prototype","CONNECTING","OPEN","CLOSING","CLOSED","RECONNECT_ABORTED","_reconnectableStatusCodes","close","force","socket","bufferedAmount","readyState","createWebSocketBackend","protocol","onopen","_onOpenHandler","bind","onmessage","_onMessageHandler","onerror","_onErrorHandler","onclose","_onCloseHandler","fireQueue","length","data","shift","send","message","JSON","stringify","deferred","resolve","notifyOpenCallbacks","i","call","notifyCloseCallbacks","event","notifyErrorCallbacks","onOpen","cb","push","onClose","onError","onMessage","callback","isFunction","Error","isDefined","filter","RegExp","fn","pattern","undefined","indexOf","code","reconnect","currentCallback","exec","cancelableify","promise","cancel","then","newPromise","apply","arguments","reason","splice","reject","defer","angular","_getBackoffDelay","attempt","R","Math","random","T","F","N","M","floor","min","pow","state","objectDefineProperty","_internalConnectionState","forEach","pending","get","set","$WebSocketBackend","$window","Socket","ws","match","exports","require","Client","client","e","WebSocket","MozWebSocket","noop","Object","freeze","defineProperty","Array","elt","len","from","Number","ceil","$inject","module","factory","service"],"mappings":"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"} \ No newline at end of file diff --git a/example/index.html b/example/index.html index 6a83ef5..2d76ef9 100644 --- a/example/index.html +++ b/example/index.html @@ -1,19 +1,32 @@ - WebSocket Test + AngularJS WebSocket Test - - - - + + + + - -
+ +

WebSocket Test

- Connection: {{ status }} -
- Message: {{ message.text }} {{ message.created_at | date }} + Connection: {{ Messages.status() }} +
+ + + +
+
+ {{ message.username | capitalize }}: {{ message.timeStamp | date:'medium' }} +
+ {{ message.content }} +
+
+
+ +
+
@@ -22,83 +35,83 @@

WebSocket Test