Permalink
Browse files

Add a chat message scenario for a public channel.

  • Loading branch information...
1 parent c4bde5f commit 95a2056a07502e03cbefceeba280e8f95f901c6e @tristandunn committed Feb 11, 2012
View
1 Gemfile
@@ -9,6 +9,7 @@ end
group :test do
gem 'bourne', '1.0'
gem 'cucumber-rails', '1.2.1', require: false
+ gem 'pusher', '0.9.2'
gem 'pusher-fake', git: 'git://github.com/tristandunn/pusher-fake.git'
gem 'rspec-rails', '2.8.1'
gem 'shoulda-matchers', '1.0.0'
View
7 Gemfile.lock
@@ -86,6 +86,10 @@ GEM
multi_json (1.0.4)
nokogiri (1.5.0)
polyglot (0.3.3)
+ pusher (0.9.2)
+ multi_json (~> 1.0)
+ ruby-hmac (~> 0.4.0)
+ signature (~> 0.1.2)
rack (1.4.1)
rack-cache (1.1)
rack (>= 0.4)
@@ -132,6 +136,8 @@ GEM
multi_json (~> 1.0.4)
rubyzip
shoulda-matchers (1.0.0)
+ signature (0.1.2)
+ ruby-hmac
sprockets (2.1.2)
hike (~> 1.2)
rack (~> 1.0)
@@ -157,6 +163,7 @@ PLATFORMS
DEPENDENCIES
bourne (= 1.0)
cucumber-rails (= 1.2.1)
+ pusher (= 0.9.2)
pusher-fake!
rails (= 3.2.1)
rspec-rails (= 2.8.1)
View
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2012 Tristan Dunn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
View
46 README.markdown
@@ -0,0 +1,46 @@
+# pusher-fake-example
+
+An example of using [pusher-fake](https://github.com/tristandunn/pusher-fake) with Cucumber to test a Rails application.
+
+## How-To
+
+### 1. Set Pusher's WebSocket host and port to PusherFake in your JavaScript.
+
+```erb
+<script>
+<% if defined?(PusherFake) %>
+ Pusher.host = <%== PusherFake.configuration.socket_host.to_json %>;
+ Pusher.ws_port = <%== PusherFake.configuration.socket_port.to_json %>;
+<% end %>
+</script>
+```
+
+### 2. Configure Pusher and PusherFake in your Cucumber environment.
+
+```ruby
+# Use the same API key and secret as the live version.
+PusherFake.configure do |configuration|
+ configuration.key = Pusher.key
+ configuration.secret = Pusher.secret
+end
+
+# Set the host and port to the fake web server.
+Pusher.host = PusherFake.configuration.web_host
+Pusher.port = PusherFake.configuration.web_port
+
+# Start the fake web server.
+Thread.new { PusherFake::Server.start }.tap do |thread|
+ at_exit { thread.exit }
+end
+
+# Reset channels between scenarios.
+After do
+ PusherFake::Channel.reset
+end
+```
+
+### 3. Enjoy testing your Pusher application!
+
+## License
+
+pusher-fake-example uses the MIT license. See LICENSE for more details.
View
1,155 app/assets/javascripts/pusher-1.11.js
@@ -0,0 +1,1155 @@
+/*!
+ * Pusher JavaScript Library v1.11.0
+ * http://pusherapp.com/
+ *
+ * Copyright 2011, Pusher
+ * Released under the MIT licence.
+ */
+
+if (Function.prototype.scopedTo === undefined) {
+ Function.prototype.scopedTo = function(context, args) {
+ var f = this;
+ return function() {
+ return f.apply(context, Array.prototype.slice.call(args || [])
+ .concat(Array.prototype.slice.call(arguments)));
+ };
+ };
+}
+
+var Pusher = function(app_key, options) {
+ this.options = options || {};
+ this.key = app_key;
+ this.channels = new Pusher.Channels();
+ this.global_emitter = new Pusher.EventsDispatcher()
+
+ var self = this;
+
+ this.checkAppKey();
+
+ this.connection = new Pusher.Connection(this.key, this.options);
+
+ // Setup / teardown connection
+ this.connection
+ .bind('connected', function() {
+ self.subscribeAll();
+ })
+ .bind('message', function(params) {
+ var internal = (params.event.indexOf('pusher_internal:') === 0);
+ if (params.channel) {
+ var channel;
+ if (channel = self.channel(params.channel)) {
+ channel.emit(params.event, params.data);
+ }
+ }
+ // Emit globaly [deprecated]
+ if (!internal) self.global_emitter.emit(params.event, params.data);
+ })
+ .bind('disconnected', function() {
+ self.channels.disconnect();
+ })
+ .bind('error', function(err) {
+ Pusher.warn('Error', err);
+ });
+
+ Pusher.instances.push(this);
+
+ if (Pusher.isReady) self.connect();
+};
+Pusher.instances = [];
+Pusher.prototype = {
+ channel: function(name) {
+ return this.channels.find(name);
+ },
+
+ connect: function() {
+ this.connection.connect();
+ },
+
+ disconnect: function() {
+ this.connection.disconnect();
+ },
+
+ bind: function(event_name, callback) {
+ this.global_emitter.bind(event_name, callback);
+ return this;
+ },
+
+ bind_all: function(callback) {
+ this.global_emitter.bind_all(callback);
+ return this;
+ },
+
+ subscribeAll: function() {
+ var channel;
+ for (channel in this.channels.channels) {
+ if (this.channels.channels.hasOwnProperty(channel)) {
+ this.subscribe(channel);
+ }
+ }
+ },
+
+ subscribe: function(channel_name) {
+ var self = this;
+ var channel = this.channels.add(channel_name, this);
+ if (this.connection.state === 'connected') {
+ channel.authorize(this, function(err, data) {
+ if (err) {
+ channel.emit('pusher:subscription_error', data);
+ } else {
+ self.send_event('pusher:subscribe', {
+ channel: channel_name,
+ auth: data.auth,
+ channel_data: data.channel_data
+ });
+ }
+ });
+ }
+ return channel;
+ },
+
+ unsubscribe: function(channel_name) {
+ this.channels.remove(channel_name);
+ if (this.connection.state === 'connected') {
+ this.send_event('pusher:unsubscribe', {
+ channel: channel_name
+ });
+ }
+ },
+
+ send_event: function(event_name, data, channel) {
+ return this.connection.send_event(event_name, data, channel);
+ },
+
+ checkAppKey: function() {
+ if(this.key === null || this.key === undefined) {
+ Pusher.warn('Warning', 'You must pass your app key when you instantiate Pusher.');
+ }
+ }
+};
+
+Pusher.Util = {
+ extend: function extend(target, extensions) {
+ for (var property in extensions) {
+ if (extensions[property] && extensions[property].constructor &&
+ extensions[property].constructor === Object) {
+ target[property] = extend(target[property] || {}, extensions[property]);
+ } else {
+ target[property] = extensions[property];
+ }
+ }
+ return target;
+ },
+
+ stringify: function stringify() {
+ var m = ["Pusher"]
+ for (var i = 0; i < arguments.length; i++){
+ if (typeof arguments[i] === "string") {
+ m.push(arguments[i])
+ } else {
+ if (window['JSON'] == undefined) {
+ m.push(arguments[i].toString());
+ } else {
+ m.push(JSON.stringify(arguments[i]))
+ }
+ }
+ };
+ return m.join(" : ")
+ },
+
+ arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf
+ var nativeIndexOf = Array.prototype.indexOf;
+ if (array == null) return -1;
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+ return -1;
+ }
+};
+
+// To receive log output provide a Pusher.log function, for example
+// Pusher.log = function(m){console.log(m)}
+Pusher.debug = function() {
+ if (!Pusher.log) return
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments))
+}
+Pusher.warn = function() {
+ if (window.console && window.console.warn) {
+ window.console.warn(Pusher.Util.stringify.apply(this, arguments));
+ } else {
+ if (!Pusher.log) return
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments));
+ }
+};
+
+// Pusher defaults
+Pusher.VERSION = '1.11.0';
+
+Pusher.host = 'ws.pusherapp.com';
+Pusher.ws_port = 80;
+Pusher.wss_port = 443;
+Pusher.channel_auth_endpoint = '/pusher/auth';
+Pusher.cdn_http = 'http://js.pusher.com/'
+Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'
+Pusher.dependency_suffix = '';
+Pusher.channel_auth_transport = 'ajax';
+Pusher.activity_timeout = 120000;
+Pusher.pong_timeout = 30000;
+
+Pusher.isReady = false;
+Pusher.ready = function() {
+ Pusher.isReady = true;
+ for (var i = 0, l = Pusher.instances.length; i < l; i++) {
+ Pusher.instances[i].connect();
+ }
+};
+
+;(function() {
+/* Abstract event binding
+Example:
+
+ var MyEventEmitter = function(){};
+ MyEventEmitter.prototype = new Pusher.EventsDispatcher;
+
+ var emitter = new MyEventEmitter();
+
+ // Bind to single event
+ emitter.bind('foo_event', function(data){ alert(data)} );
+
+ // Bind to all
+ emitter.bind_all(function(event_name, data){ alert(data) });
+
+--------------------------------------------------------*/
+ function EventsDispatcher(failThrough) {
+ this.callbacks = {};
+ this.global_callbacks = [];
+ // Run this function when dispatching an event when no callbacks defined
+ this.failThrough = failThrough;
+ }
+
+ EventsDispatcher.prototype.bind = function(event_name, callback) {
+ this.callbacks[event_name] = this.callbacks[event_name] || [];
+ this.callbacks[event_name].push(callback);
+ return this;// chainable
+ };
+
+ EventsDispatcher.prototype.unbind = function(eventName, callback) {
+ if(this.callbacks[eventName]) {
+ var index = Pusher.Util.arrayIndexOf(this.callbacks[eventName], callback);
+ this.callbacks[eventName].splice(index, 1);
+ }
+ return this;
+ };
+
+ EventsDispatcher.prototype.emit = function(event_name, data) {
+ // Global callbacks
+ for (var i = 0; i < this.global_callbacks.length; i++) {
+ this.global_callbacks[i](event_name, data);
+ }
+
+ // Event callbacks
+ var callbacks = this.callbacks[event_name];
+ if (callbacks) {
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i](data);
+ }
+ } else if (this.failThrough) {
+ this.failThrough(event_name, data)
+ }
+
+ return this;
+ };
+
+ EventsDispatcher.prototype.bind_all = function(callback) {
+ this.global_callbacks.push(callback);
+ return this;
+ };
+
+ this.Pusher.EventsDispatcher = EventsDispatcher;
+}).call(this);
+
+;(function() {
+ var Pusher = this.Pusher;
+
+ /*-----------------------------------------------
+ Helpers:
+ -----------------------------------------------*/
+
+ function capitalize(str) {
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
+ }
+
+
+ function safeCall(method, obj, data) {
+ if (obj[method] !== undefined) {
+ obj[method](data);
+ }
+ }
+
+ /*-----------------------------------------------
+ The State Machine
+ -----------------------------------------------*/
+ function Machine(initialState, transitions, stateActions) {
+ Pusher.EventsDispatcher.call(this);
+
+ this.state = undefined;
+ this.errors = [];
+
+ // functions for each state
+ this.stateActions = stateActions;
+
+ // set up the transitions
+ this.transitions = transitions;
+
+ this.transition(initialState);
+ };
+
+ Machine.prototype.transition = function(nextState, data) {
+ var prevState = this.state;
+ var stateCallbacks = this.stateActions;
+
+ if (prevState && (Pusher.Util.arrayIndexOf(this.transitions[prevState], nextState) == -1)) {
+ throw new Error('Invalid transition [' + prevState + ' to ' + nextState + ']');
+ }
+
+ // exit
+ safeCall(prevState + 'Exit', stateCallbacks, data);
+
+ // tween
+ safeCall(prevState + 'To' + capitalize(nextState), stateCallbacks, data);
+
+ // pre
+ safeCall(nextState + 'Pre', stateCallbacks, data);
+
+ // change state:
+ this.state = nextState;
+
+ // handy to bind to
+ this.emit('state_change', {
+ oldState: prevState,
+ newState: nextState
+ });
+
+ // Post:
+ safeCall(nextState + 'Post', stateCallbacks, data);
+ };
+
+ Machine.prototype.is = function(state) {
+ return this.state === state;
+ };
+
+ Machine.prototype.isNot = function(state) {
+ return this.state !== state;
+ };
+
+ Pusher.Util.extend(Machine.prototype, Pusher.EventsDispatcher.prototype);
+
+ this.Pusher.Machine = Machine;
+}).call(this);
+
+;(function() {
+ /*
+ A little bauble to interface with window.navigator.onLine,
+ window.ononline and window.onoffline. Easier to mock.
+ */
+ var NetInfo = function() {
+ var self = this;
+ Pusher.EventsDispatcher.call(this);
+ // This is okay, as IE doesn't support this stuff anyway.
+ if (window.addEventListener !== undefined) {
+ window.addEventListener("online", function() {
+ self.emit('online', null);
+ }, false);
+ window.addEventListener("offline", function() {
+ self.emit('offline', null);
+ }, false);
+ }
+ };
+
+ // Offline means definitely offline (no connection to router).
+ // Inverse does NOT mean definitely online (only currently supported in Safari
+ // and even there only means the device has a connection to the router).
+ NetInfo.prototype.isOnLine = function() {
+ if (window.navigator.onLine === undefined) {
+ return true;
+ } else {
+ return window.navigator.onLine;
+ }
+ };
+
+ Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
+
+ this.Pusher.NetInfo = NetInfo;
+}).call(this);
+
+;(function() {
+ var Pusher = this.Pusher;
+
+ var machineTransitions = {
+ 'initialized': ['waiting', 'failed'],
+ 'waiting': ['connecting', 'permanentlyClosed'],
+ 'connecting': ['open', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'open': ['connected', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'connected': ['permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'impermanentlyClosing': ['waiting', 'permanentlyClosing'],
+ 'permanentlyClosing': ['permanentlyClosed'],
+ 'permanentlyClosed': ['waiting'],
+ 'failed': ['permanentlyClosing']
+ };
+
+
+ // Amount to add to time between connection attemtpts per failed attempt.
+ var UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT = 2000;
+ var UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
+ var UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
+
+ var MAX_CONNECTION_ATTEMPT_WAIT = 5 * UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
+ var MAX_OPEN_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
+ var MAX_CONNECTED_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
+
+ function resetConnectionParameters(connection) {
+ connection.connectionWait = 0;
+
+ if (Pusher.TransportType === 'flash') {
+ // Flash needs a bit more time
+ connection.openTimeout = 5000;
+ } else {
+ connection.openTimeout = 2000;
+ }
+ connection.connectedTimeout = 2000;
+ connection.connectionSecure = connection.compulsorySecure;
+ connection.connectionAttempts = 0;
+ }
+
+ function Connection(key, options) {
+ var self = this;
+
+ Pusher.EventsDispatcher.call(this);
+
+ this.options = Pusher.Util.extend({encrypted: false}, options);
+
+ this.netInfo = new Pusher.NetInfo();
+
+ this.netInfo.bind('online', function(){
+ if (self._machine.is('waiting')) {
+ self._machine.transition('connecting');
+ triggerStateChange('connecting');
+ }
+ });
+
+ this.netInfo.bind('offline', function() {
+ if (self._machine.is('connected')) {
+ // These are for Chrome 15, which ends up
+ // having two sockets hanging around.
+ self.socket.onclose = undefined;
+ self.socket.onmessage = undefined;
+ self.socket.onerror = undefined;
+ self.socket.onopen = undefined;
+
+ self.socket.close();
+ self.socket = undefined;
+ self._machine.transition('waiting');
+ }
+ });
+
+ // define the state machine that runs the connection
+ this._machine = new Pusher.Machine('initialized', machineTransitions, {
+
+ // TODO: Use the constructor for this.
+ initializedPre: function() {
+ self.compulsorySecure = self.options.encrypted;
+
+ self.key = key;
+ self.socket = null;
+ self.socket_id = null;
+
+ self.state = 'initialized';
+ },
+
+ waitingPre: function() {
+ if (self.connectionWait > 0) {
+ self.emit('connecting_in', self.connectionWait);
+ }
+
+ if (self.netInfo.isOnLine() === false || self.connectionAttempts > 4){
+ triggerStateChange('unavailable');
+ } else {
+ triggerStateChange('connecting');
+ }
+
+ if (self.netInfo.isOnLine() === true) {
+ self._waitingTimer = setTimeout(function() {
+ self._machine.transition('connecting');
+ }, self.connectionWait);
+ }
+ },
+
+ waitingExit: function() {
+ clearTimeout(self._waitingTimer);
+ },
+
+ connectingPre: function() {
+ // Case that a user manages to get to the connecting
+ // state even when offline.
+ if (self.netInfo.isOnLine() === false) {
+ self._machine.transition('waiting');
+ triggerStateChange('unavailable');
+
+ return;
+ }
+
+ // removed: if not closed, something is wrong that we should fix
+ // if(self.socket !== undefined) self.socket.close();
+ var url = formatURL(self.key, self.connectionSecure);
+ Pusher.debug('Connecting', url);
+ self.socket = new Pusher.Transport(url);
+ // now that the socket connection attempt has been started,
+ // set up the callbacks fired by the socket for different outcomes
+ self.socket.onopen = ws_onopen;
+ self.socket.onclose = transitionToWaiting;
+ self.socket.onerror = ws_onError;
+
+ // allow time to get ws_onOpen, otherwise close socket and try again
+ self._connectingTimer = setTimeout(TransitionToImpermanentClosing, self.openTimeout);
+ },
+
+ connectingExit: function() {
+ clearTimeout(self._connectingTimer);
+ },
+
+ connectingToWaiting: function() {
+ updateConnectionParameters();
+
+ // FUTURE: update only ssl
+ },
+
+ connectingToImpermanentlyClosing: function() {
+ updateConnectionParameters();
+
+ // FUTURE: update only timeout
+ },
+
+ openPre: function() {
+ self.socket.onmessage = ws_onMessageOpen;
+ self.socket.onerror = ws_onError;
+ self.socket.onclose = transitionToWaiting;
+
+ // allow time to get connected-to-Pusher message, otherwise close socket, try again
+ self._openTimer = setTimeout(TransitionToImpermanentClosing, self.connectedTimeout);
+ },
+
+ openExit: function() {
+ clearTimeout(self._openTimer);
+ },
+
+ openToWaiting: function() {
+ updateConnectionParameters();
+ },
+
+ openToImpermanentlyClosing: function() {
+ updateConnectionParameters();
+ },
+
+ connectedPre: function(socket_id) {
+ self.socket_id = socket_id;
+
+ self.socket.onmessage = ws_onMessageConnected;
+ self.socket.onerror = ws_onError;
+ self.socket.onclose = transitionToWaiting;
+
+ resetConnectionParameters(self);
+
+ resetActivityCheck();
+ },
+
+ connectedPost: function() {
+ triggerStateChange('connected');
+ },
+
+ connectedExit: function() {
+ stopActivityCheck();
+ triggerStateChange('disconnected');
+ },
+
+ impermanentlyClosingPost: function() {
+ if (self.socket) {
+ self.socket.onclose = transitionToWaiting;
+ self.socket.close();
+ }
+ },
+
+ permanentlyClosingPost: function() {
+ if (self.socket) {
+ self.socket.onclose = function() {
+ resetConnectionParameters(self);
+ self._machine.transition('permanentlyClosed');
+ };
+
+ self.socket.close();
+ } else {
+ resetConnectionParameters(self);
+ self._machine.transition('permanentlyClosed');
+ }
+ },
+
+ failedPre: function() {
+ triggerStateChange('failed');
+ Pusher.debug('WebSockets are not available in this browser.');
+ }
+ });
+
+ /*-----------------------------------------------
+ -----------------------------------------------*/
+
+ function updateConnectionParameters() {
+ if (self.connectionWait < MAX_CONNECTION_ATTEMPT_WAIT) {
+ self.connectionWait += UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
+ }
+
+ if (self.openTimeout < MAX_OPEN_ATTEMPT_TIMEOUT) {
+ self.openTimeout += UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
+ }
+
+ if (self.connectedTimeout < MAX_CONNECTED_ATTEMPT_TIMEOUT) {
+ self.connectedTimeout += UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
+ }
+
+ if (self.compulsorySecure !== true) {
+ self.connectionSecure = !self.connectionSecure;
+ }
+
+ self.connectionAttempts++;
+ }
+
+ function formatURL(key, isSecure) {
+ var port = Pusher.ws_port;
+ var protocol = 'ws://';
+
+ // Always connect with SSL if the current page has
+ // been loaded via HTTPS.
+ //
+ // FUTURE: Always connect using SSL.
+ //
+ if (isSecure || document.location.protocol === 'https:') {
+ port = Pusher.wss_port;
+ protocol = 'wss://';
+ }
+
+ return protocol + Pusher.host + ':' + port + '/app/' + key + '?client=js&version=' + Pusher.VERSION;
+ }
+
+ // callback for close and retry. Used on timeouts.
+ function TransitionToImpermanentClosing() {
+ self._machine.transition('impermanentlyClosing');
+ }
+
+ function resetActivityCheck() {
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
+ // Send ping after inactivity
+ self._activityTimer = setTimeout(function() {
+ self.send_event('pusher:ping', {})
+ // Wait for pong response
+ self._activityTimer = setTimeout(function() {
+ self.socket.close();
+ }, (self.options.pong_timeout || Pusher.pong_timeout))
+ }, (self.options.activity_timeout || Pusher.activity_timeout))
+ }
+
+ function stopActivityCheck() {
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
+ }
+
+ /*-----------------------------------------------
+ WebSocket Callbacks
+ -----------------------------------------------*/
+
+ // no-op, as we only care when we get pusher:connection_established
+ function ws_onopen() {
+ self._machine.transition('open');
+ };
+
+ function ws_onMessageOpen(event) {
+ var params = parseWebSocketEvent(event);
+ if (params !== undefined) {
+ if (params.event === 'pusher:connection_established') {
+ self._machine.transition('connected', params.data.socket_id);
+ } else if (params.event === 'pusher:error') {
+ // first inform the end-developer of this error
+ self.emit('error', {type: 'PusherError', data: params.data});
+
+ switch (params.data.code) {
+ case 4000:
+ Pusher.warn(params.data.message);
+
+ self.compulsorySecure = true;
+ self.connectionSecure = true;
+ self.options.encrypted = true;
+ break;
+ case 4001:
+ // App not found by key - close connection
+ self._machine.transition('permanentlyClosing');
+ break;
+ }
+ }
+ }
+ }
+
+ function ws_onMessageConnected(event) {
+ resetActivityCheck();
+
+ var params = parseWebSocketEvent(event);
+ if (params !== undefined) {
+ Pusher.debug('Event recd', params);
+
+ switch (params.event) {
+ case 'pusher:error':
+ self.emit('error', {type: 'PusherError', data: params.data});
+ break;
+ case 'pusher:ping':
+ self.send_event('pusher:pong', {})
+ break;
+ }
+
+ self.emit('message', params);
+ }
+ }
+
+
+ /**
+ * Parses an event from the WebSocket to get
+ * the JSON payload that we require
+ *
+ * @param {MessageEvent} event The event from the WebSocket.onmessage handler.
+ **/
+ function parseWebSocketEvent(event) {
+ try {
+ var params = JSON.parse(event.data);
+
+ if (typeof params.data === 'string') {
+ try {
+ params.data = JSON.parse(params.data);
+ } catch (e) {
+ if (!(e instanceof SyntaxError)) {
+ throw e;
+ }
+ }
+ }
+
+ return params;
+ } catch (e) {
+ self.emit('error', {type: 'MessageParseError', error: e, data: event.data});
+ }
+ }
+
+ function transitionToWaiting() {
+ self._machine.transition('waiting');
+ }
+
+ function ws_onError() {
+ self.emit('error', {
+ type: 'WebSocketError'
+ });
+
+ // note: required? is the socket auto closed in the case of error?
+ self.socket.close();
+ self._machine.transition('impermanentlyClosing');
+ }
+
+ function triggerStateChange(newState, data) {
+ // avoid emitting and changing the state
+ // multiple times when it's the same.
+ if (self.state === newState) return;
+
+ var prevState = self.state;
+
+ self.state = newState;
+
+ Pusher.debug('State changed', prevState + ' -> ' + newState);
+
+ self.emit('state_change', {previous: prevState, current: newState});
+ self.emit(newState, data);
+ }
+ };
+
+ Connection.prototype.connect = function() {
+ // no WebSockets
+ if (Pusher.Transport === null || Pusher.Transport === undefined) {
+ this._machine.transition('failed');
+ }
+ // initial open of connection
+ else if(this._machine.is('initialized')) {
+ resetConnectionParameters(this);
+ this._machine.transition('waiting');
+ }
+ // user skipping connection wait
+ else if (this._machine.is('waiting') && this.netInfo.isOnLine() === true) {
+ this._machine.transition('connecting');
+ }
+ // user re-opening connection after closing it
+ else if(this._machine.is("permanentlyClosed")) {
+ this._machine.transition('waiting');
+ }
+ };
+
+ Connection.prototype.send = function(data) {
+ if (this._machine.is('connected')) {
+ this.socket.send(data);
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ Connection.prototype.send_event = function(event_name, data, channel) {
+ var payload = {
+ event: event_name,
+ data: data
+ };
+ if (channel) payload['channel'] = channel;
+
+ Pusher.debug('Event sent', payload);
+ return this.send(JSON.stringify(payload));
+ }
+
+ Connection.prototype.disconnect = function() {
+ if (this._machine.is('permanentlyClosed')) return;
+
+ if (this._machine.is('waiting')) {
+ this._machine.transition('permanentlyClosed');
+ } else {
+ this._machine.transition('permanentlyClosing');
+ }
+ };
+
+ Pusher.Util.extend(Connection.prototype, Pusher.EventsDispatcher.prototype);
+ this.Pusher.Connection = Connection;
+}).call(this);
+
+Pusher.Channels = function() {
+ this.channels = {};
+};
+
+Pusher.Channels.prototype = {
+ add: function(channel_name, pusher) {
+ var existing_channel = this.find(channel_name);
+ if (!existing_channel) {
+ var channel = Pusher.Channel.factory(channel_name, pusher);
+ this.channels[channel_name] = channel;
+ return channel;
+ } else {
+ return existing_channel;
+ }
+ },
+
+ find: function(channel_name) {
+ return this.channels[channel_name];
+ },
+
+ remove: function(channel_name) {
+ delete this.channels[channel_name];
+ },
+
+ disconnect: function () {
+ for(var channel_name in this.channels){
+ this.channels[channel_name].disconnect()
+ }
+ }
+};
+
+Pusher.Channel = function(channel_name, pusher) {
+ var self = this;
+ Pusher.EventsDispatcher.call(this, function(event_name, event_data) {
+ Pusher.debug('No callbacks on ' + channel_name + ' for ' + event_name);
+ });
+
+ this.pusher = pusher;
+ this.name = channel_name;
+ this.subscribed = false;
+
+ this.bind('pusher_internal:subscription_succeeded', function(data) {
+ self.onSubscriptionSucceeded(data);
+ });
+};
+
+Pusher.Channel.prototype = {
+ // inheritable constructor
+ init: function() {},
+ disconnect: function() {},
+
+ onSubscriptionSucceeded: function(data) {
+ this.subscribed = true;
+ this.emit('pusher:subscription_succeeded');
+ },
+
+ authorize: function(pusher, callback){
+ callback(false, {}); // normal channels don't require auth
+ },
+
+ trigger: function(event, data) {
+ return this.pusher.send_event(event, data, this.name);
+ }
+};
+
+Pusher.Util.extend(Pusher.Channel.prototype, Pusher.EventsDispatcher.prototype);
+
+
+
+Pusher.auth_callbacks = {};
+
+Pusher.authorizers = {
+ ajax: function(pusher, callback){
+ var self = this, xhr;
+
+ if (Pusher.XHR) {
+ xhr = new Pusher.XHR();
+ } else {
+ xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
+ }
+
+ xhr.open("POST", Pusher.channel_auth_endpoint, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ var data, parsed = false;
+
+ try {
+ data = JSON.parse(xhr.responseText);
+ parsed = true;
+ } catch (e) {
+ callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText);
+ }
+
+ if (parsed) { // prevents double execution.
+ callback(false, data);
+ }
+ } else {
+ Pusher.warn("Couldn't get auth info from your webapp", status);
+ callback(true, xhr.status);
+ }
+ }
+ };
+ xhr.send('socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(self.name));
+ },
+ jsonp: function(pusher, callback){
+ var qstring = 'socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(this.name);
+ var script = document.createElement("script");
+ // Hacked wrapper.
+ Pusher.auth_callbacks[this.name] = function(data) {
+ callback(false, data);
+ };
+ var callback_name = "Pusher.auth_callbacks['" + this.name + "']";
+ script.src = Pusher.channel_auth_endpoint+'?callback='+encodeURIComponent(callback_name)+'&'+qstring;
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
+ head.insertBefore( script, head.firstChild );
+ }
+};
+
+Pusher.Channel.PrivateChannel = {
+ authorize: function(pusher, callback){
+ Pusher.authorizers[Pusher.channel_auth_transport].scopedTo(this)(pusher, callback);
+ }
+};
+
+Pusher.Channel.PresenceChannel = {
+ init: function(){
+ this.bind('pusher_internal:member_added', function(data){
+ var member = this.members.add(data.user_id, data.user_info);
+ this.emit('pusher:member_added', member);
+ }.scopedTo(this))
+
+ this.bind('pusher_internal:member_removed', function(data){
+ var member = this.members.remove(data.user_id);
+ if (member) {
+ this.emit('pusher:member_removed', member);
+ }
+ }.scopedTo(this))
+ },
+
+ disconnect: function(){
+ this.members.clear();
+ },
+
+ onSubscriptionSucceeded: function(data) {
+ this.members._members_map = data.presence.hash;
+ this.members.count = data.presence.count;
+ this.subscribed = true;
+
+ this.emit('pusher:subscription_succeeded', this.members);
+ },
+
+ members: {
+ _members_map: {},
+ count: 0,
+
+ each: function(callback) {
+ for(var i in this._members_map) {
+ callback({
+ id: i,
+ info: this._members_map[i]
+ });
+ }
+ },
+
+ add: function(id, info) {
+ this._members_map[id] = info;
+ this.count++;
+ return this.get(id);
+ },
+
+ remove: function(user_id) {
+ var member = this.get(user_id);
+ if (member) {
+ delete this._members_map[user_id];
+ this.count--;
+ }
+ return member;
+ },
+
+ get: function(user_id) {
+ if (this._members_map.hasOwnProperty(user_id)) { // have heard of this user user_id
+ return {
+ id: user_id,
+ info: this._members_map[user_id]
+ }
+ } else { // have never heard of this user
+ return null;
+ }
+ },
+
+ clear: function() {
+ this._members_map = {};
+ this.count = 0;
+ }
+ }
+};
+
+Pusher.Channel.factory = function(channel_name, pusher){
+ var channel = new Pusher.Channel(channel_name, pusher);
+ if (channel_name.indexOf('private-') === 0) {
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
+ } else if (channel_name.indexOf('presence-') === 0) {
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
+ Pusher.Util.extend(channel, Pusher.Channel.PresenceChannel);
+ };
+ channel.init();
+ return channel;
+};
+
+var _require = (function () {
+
+ var handleScriptLoaded;
+ if (document.addEventListener) {
+ handleScriptLoaded = function (elem, callback) {
+ elem.addEventListener('load', callback, false)
+ }
+ } else {
+ handleScriptLoaded = function(elem, callback) {
+ elem.attachEvent('onreadystatechange', function () {
+ if(elem.readyState == 'loaded' || elem.readyState == 'complete') callback()
+ })
+ }
+ }
+
+ return function (deps, callback) {
+ var dep_count = 0,
+ dep_length = deps.length;
+
+ function checkReady (callback) {
+ dep_count++;
+ if ( dep_length == dep_count ) {
+ // Opera needs the timeout for page initialization weirdness
+ setTimeout(callback, 0);
+ }
+ }
+
+ function addScript (src, callback) {
+ callback = callback || function(){}
+ var head = document.getElementsByTagName('head')[0];
+ var script = document.createElement('script');
+ script.setAttribute('src', src);
+ script.setAttribute("type","text/javascript");
+ script.setAttribute('async', true);
+
+ handleScriptLoaded(script, function () {
+ checkReady(callback);
+ });
+
+ head.appendChild(script);
+ }
+
+ for(var i = 0; i < dep_length; i++) {
+ addScript(deps[i], callback);
+ }
+ }
+})();
+
+;(function() {
+ var cdn = (document.location.protocol == 'http:') ? Pusher.cdn_http : Pusher.cdn_https;
+ var root = cdn + Pusher.VERSION;
+ var deps = [];
+
+ if (window['JSON'] === undefined) {
+ deps.push(root + '/json2' + Pusher.dependency_suffix + '.js');
+ }
+ if (window['WebSocket'] === undefined && window['MozWebSocket'] === undefined) {
+ // We manually initialize web-socket-js to iron out cross browser issues
+ window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
+ deps.push(root + '/flashfallback' + Pusher.dependency_suffix + '.js');
+ }
+
+ var initialize = function() {
+ if (window['WebSocket'] === undefined && window['MozWebSocket'] === undefined) {
+ return function() {
+ // This runs after flashfallback.js has loaded
+ if (window['WebSocket'] !== undefined && window['MozWebSocket'] === undefined) {
+ // window['WebSocket'] is a flash emulation of WebSocket
+ Pusher.Transport = window['WebSocket'];
+ Pusher.TransportType = 'flash';
+
+ window.WEB_SOCKET_SWF_LOCATION = root + "/WebSocketMain.swf";
+ WebSocket.__addTask(function() {
+ Pusher.ready();
+ })
+ WebSocket.__initialize();
+ } else {
+ // Flash must not be installed
+ Pusher.Transport = null;
+ Pusher.TransportType = 'none';
+ Pusher.ready();
+ }
+ }
+ } else {
+ return function() {
+ // This is because Mozilla have decided to
+ // prefix the WebSocket constructor with "Moz".
+ if (window['MozWebSocket'] !== undefined) {
+ Pusher.Transport = window['MozWebSocket'];
+ } else {
+ Pusher.Transport = window['WebSocket'];
+ }
+ // We have some form of a native websocket,
+ // even if the constructor is prefixed:
+ Pusher.TransportType = 'native';
+
+ // Initialise Pusher.
+ Pusher.ready();
+ }
+ }
+ }();
+
+ var ondocumentbody = function(callback) {
+ var load_body = function() {
+ document.body ? callback() : setTimeout(load_body, 0);
+ }
+ load_body();
+ };
+
+ var initializeOnDocumentBody = function() {
+ ondocumentbody(initialize);
+ }
+
+ if (deps.length > 0) {
+ _require(deps, initializeOnDocumentBody);
+ } else {
+ initializeOnDocumentBody();
+ }
+})();
View
2 app/controllers/pages_controller.rb
@@ -0,0 +1,2 @@
+class PagesController < ApplicationController
+end
View
16 app/views/pages/index.html.erb
@@ -0,0 +1,16 @@
+<script>
+ window.addEventListener("DOMContentLoaded", function() {
+ <% if defined?(PusherFake) %>
+ Pusher.host = <%== PusherFake.configuration.socket_host.to_json %>;
+ Pusher.ws_port = <%== PusherFake.configuration.socket_port.to_json %>;
+ <% end %>
+
+ Pusher.instance = new Pusher(<%== Pusher.key.to_json %>);
+
+ var
+ Chat = Pusher.instance.subscribe("chat");
+ Chat.bind("message", function(message) {
+ document.body.innerHTML = message.body;
+ });
+ }, false);
+</script>
View
3 config/initializers/pusher.rb
@@ -0,0 +1,3 @@
+Pusher.app_id = "PUSHER_APP_ID"
+Pusher.key = "PUSHER_API_KEY"
+Pusher.secret = "PUSHER_API_SECRET"
View
57 config/routes.rb
@@ -1,58 +1,3 @@
PusherFakeExample::Application.routes.draw do
- # The priority is based upon order of creation:
- # first created -> highest priority.
-
- # Sample of regular route:
- # match 'products/:id' => 'catalog#view'
- # Keep in mind you can assign values other than :controller and :action
-
- # Sample of named route:
- # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
- # This route can be invoked with purchase_url(:id => product.id)
-
- # Sample resource route (maps HTTP verbs to controller actions automatically):
- # resources :products
-
- # Sample resource route with options:
- # resources :products do
- # member do
- # get 'short'
- # post 'toggle'
- # end
- #
- # collection do
- # get 'sold'
- # end
- # end
-
- # Sample resource route with sub-resources:
- # resources :products do
- # resources :comments, :sales
- # resource :seller
- # end
-
- # Sample resource route with more complex sub-resources
- # resources :products do
- # resources :comments
- # resources :sales do
- # get 'recent', :on => :collection
- # end
- # end
-
- # Sample resource route within a namespace:
- # namespace :admin do
- # # Directs /admin/products/* to Admin::ProductsController
- # # (app/controllers/admin/products_controller.rb)
- # resources :products
- # end
-
- # You can have the root of your site routed with "root"
- # just remember to delete public/index.html.
- # root :to => 'welcome#index'
-
- # See how all your routes lay out with "rake routes"
-
- # This is a legacy wild controller route that's not recommended for RESTful applications.
- # Note: This route will make all actions in every controller accessible via GET requests.
- # match ':controller(/:action(/:id))(.:format)'
+ root to: "Pages#index"
end
View
11 features/message.feature
@@ -0,0 +1,11 @@
+@javascript
+Feature: Server triggers a message
+
+ Scenario: Server triggers a message on the chat channel
+ Given I am connected
+ And Bob is connected
+ When a "message" event is triggered on the "chat" channel with:
+ | body |
+ | Hello, world! |
+ Then I should see "Hello, world!"
+ And Bob should see "Hello, world!"
View
17 features/step_definitions/client_steps.rb
@@ -0,0 +1,17 @@
+Given "I am connected" do
+ step %{I am on the homepage}
+ step %{I should be connected}
+end
+
+Given %{$name is connected} do |name|
+ using_session(name) do
+ step %{I am connected}
+ end
+end
+
+Then "I should be connected" do
+ wait_until do
+ state = page.evaluate_script("Pusher.instance.connection.state")
+ state == "connected"
+ end
+end
View
3 features/step_definitions/event_steps.rb
@@ -0,0 +1,3 @@
+When %{a "$event" event is triggered on the "$channel" channel with:} do |event, channel, table|
+ Pusher[channel].trigger(event, table.hashes.first)
+end
View
9 features/step_definitions/html_steps.rb
@@ -0,0 +1,9 @@
+Then /^I should see "([^"]*)"$/ do |text|
+ page.should have_content(text)
+end
+
+Then /^([^I]+) should see "([^"]*)"$/ do |name, text|
+ using_session(name) do
+ step %{I should see "#{text}"}
+ end
+end
View
3 features/step_definitions/navigation_steps.rb
@@ -0,0 +1,3 @@
+Given "I am on the homepage" do
+ visit "/"
+end
View
19 features/support/pusher-fake.rb
@@ -0,0 +1,19 @@
+# Use the same API key and secret as the live version.
+PusherFake.configure do |configuration|
+ configuration.key = Pusher.key
+ configuration.secret = Pusher.secret
+end
+
+# Set the host and port to the fake web server.
+Pusher.host = PusherFake.configuration.web_host
+Pusher.port = PusherFake.configuration.web_port
+
+# Start the fake web server.
+Thread.new { PusherFake::Server.start }.tap do |thread|
+ at_exit { thread.exit }
+end
+
+# Reset channels between scenarios.
+After do
+ PusherFake::Channel.reset
+end

0 comments on commit 95a2056

Please sign in to comment.