pusher-fake 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/features/channel_presence.feature +21 -0
  2. data/features/channel_subscribe.feature +33 -0
  3. data/features/channel_trigger.feature +74 -0
  4. data/features/client_connect.feature +8 -0
  5. data/features/step_definitions/channel_steps.rb +82 -0
  6. data/features/step_definitions/client_steps.rb +28 -0
  7. data/features/step_definitions/event_steps.rb +41 -0
  8. data/features/step_definitions/navigation_steps.rb +3 -0
  9. data/features/step_definitions/presence_steps.rb +6 -0
  10. data/features/support/application.rb +29 -0
  11. data/features/support/application/public/javascripts/vendor/pusher-1.11.js +1155 -0
  12. data/features/support/application/views/index.erb +43 -0
  13. data/features/support/environment.rb +12 -0
  14. data/features/support/pusher-fake.rb +7 -0
  15. data/features/support/wait.rb +9 -0
  16. data/lib/pusher-fake.rb +35 -0
  17. data/lib/pusher-fake/channel.rb +51 -0
  18. data/lib/pusher-fake/channel/presence.rb +50 -0
  19. data/lib/pusher-fake/channel/private.rb +44 -0
  20. data/lib/pusher-fake/channel/public.rb +63 -0
  21. data/lib/pusher-fake/configuration.rb +31 -0
  22. data/lib/pusher-fake/connection.rb +54 -0
  23. data/lib/pusher-fake/server.rb +48 -0
  24. data/lib/pusher-fake/server/application.rb +60 -0
  25. data/spec/lib/pusher-fake/channel/presence_spec.rb +133 -0
  26. data/spec/lib/pusher-fake/channel/private_spec.rb +125 -0
  27. data/spec/lib/pusher-fake/channel/public_spec.rb +90 -0
  28. data/spec/lib/pusher-fake/channel_spec.rb +117 -0
  29. data/spec/lib/pusher-fake/configuration_spec.rb +10 -0
  30. data/spec/lib/pusher-fake/connection_spec.rb +193 -0
  31. data/spec/lib/pusher-fake/server/application_spec.rb +133 -0
  32. data/spec/lib/pusher-fake/server_spec.rb +150 -0
  33. data/spec/spec_helper.rb +12 -0
  34. data/spec/support/have_configuration_option_matcher.rb +19 -0
  35. metadata +265 -0
@@ -0,0 +1,21 @@
1
+ @javascript
2
+ Feature: Client on a presence channel
3
+
4
+ Background:
5
+ Given I am connected
6
+
7
+ Scenario: Client subscribes to a presence channel
8
+ When I subscribe to the "presence-game-1" channel with presence events
9
+ Then I should see 1 client
10
+ When Bob is connected
11
+ Then I should see 1 client
12
+ When Bob is subscribed to the "presence-game-1" channel
13
+ Then I should see 2 clients
14
+
15
+ Scenario: Client unsubscribes from a presence channel, with other clients
16
+ Given Bob is connected
17
+ And Bob is subscribed to the "presence-game-1" channel
18
+ When I subscribe to the "presence-game-1" channel with presence events
19
+ Then I should see 2 clients
20
+ When Bob unsubscribes from the "presence-game-1" channel
21
+ Then I should see 1 client
@@ -0,0 +1,33 @@
1
+ @javascript
2
+ Feature: Client subscribing to a channel
3
+
4
+ Background:
5
+ Given I am connected
6
+
7
+ Scenario: Client subscribes to a channel
8
+ When I subscribe to the "chat-message" channel
9
+ Then I should be subscribed to the "chat-message" channel
10
+
11
+ Scenario: Client subscribes to multiple channels
12
+ When I subscribe to the "chat-enter" channel
13
+ And I subscribe to the "chat-exit" channel
14
+ Then I should be subscribed to the "chat-enter" channel
15
+ And I should be subscribed to the "chat-exit" channel
16
+
17
+ Scenario: Client subscribes to a private channel
18
+ When I subscribe to the "private-message-bob" channel
19
+ Then I should be subscribed to the "private-message-bob" channel
20
+
21
+ Scenario: Client unsuccessfully subscribes to a private channel
22
+ Given I change my socket ID
23
+ When I subscribe to the "private-message-bob" channel
24
+ Then I should not be subscribed to the "private-message-bob" channel
25
+
26
+ Scenario: Client subscribes to a presence channel
27
+ When I subscribe to the "presence-game-1" channel
28
+ Then I should be subscribed to the "presence-game-1" channel
29
+
30
+ Scenario: Client unsuccessfully subscribes to a presence channel
31
+ Given I change my socket ID
32
+ When I subscribe to the "presence-game-1" channel
33
+ Then I should not be subscribed to the "presence-game-1" channel
@@ -0,0 +1,74 @@
1
+ @javascript
2
+ Feature: Triggering events on a channel
3
+
4
+ Background:
5
+ Given I am connected
6
+ And Bob is connected
7
+
8
+ Scenario: Server triggers an event on a subscribed public channel
9
+ Given I am subscribed to the "chat" channel
10
+ And Bob is subscribed to the "chat" channel
11
+ When a "message" event is triggered on the "chat" channel
12
+ Then I should receive a "message" event on the "chat" channel
13
+ And Bob should receive a "message" event on the "chat" channel
14
+
15
+ Scenario: Server triggers an event on a previously subscribed public channel
16
+ Given I am subscribed to the "chat" channel
17
+ And Bob is subscribed to the "chat" channel
18
+ And I unsubscribe from the "chat" channel
19
+ When a "message" event is triggered on the "chat" channel
20
+ Then I should not receive a "message" event on the "chat" channel
21
+ And Bob should receive a "message" event on the "chat" channel
22
+
23
+ Scenario: Server triggers an event on an unsubscribed public channel
24
+ When a "message" event is triggered on the "chat" channel
25
+ Then I should not receive a "message" event on the "chat" channel
26
+ And Bob should not receive a "message" event on the "chat" channel
27
+
28
+ Scenario: Server triggers an event on a subscribed private channel
29
+ Given I am subscribed to the "private-chat" channel
30
+ And Bob is subscribed to the "private-chat" channel
31
+ When a "message" event is triggered on the "private-chat" channel
32
+ Then I should receive a "message" event on the "private-chat" channel
33
+ And Bob should receive a "message" event on the "private-chat" channel
34
+
35
+ Scenario: Server triggers an event on a previously subscribed private channel
36
+ Given I am subscribed to the "private-chat" channel
37
+ And Bob is subscribed to the "private-chat" channel
38
+ And I unsubscribe from the "private-chat" channel
39
+ When a "message" event is triggered on the "private-chat" channel
40
+ Then I should not receive a "message" event on the "private-chat" channel
41
+ And Bob should receive a "message" event on the "private-chat" channel
42
+
43
+ Scenario: Server triggers an event on an unsubscribed private channel
44
+ When a "message" event is triggered on the "private-chat" channel
45
+ Then I should not receive a "message" event on the "private-chat" channel
46
+ And Bob should not receive a "message" event on the "private-chat" channel
47
+
48
+ Scenario: Client triggers a client event on a subscribed private channel
49
+ Given I am subscribed to the "private-chat" channel
50
+ And Bob is subscribed to the "private-chat" channel
51
+ When I trigger the "client-message" event on the "private-chat" channel
52
+ Then I should receive a "client-message" event on the "private-chat" channel
53
+ And Bob should receive a "client-message" event on the "private-chat" channel
54
+
55
+ Scenario: Client triggers a client event on a previously subscribed private channel
56
+ Given I am subscribed to the "private-chat" channel
57
+ And Bob is subscribed to the "private-chat" channel
58
+ And I unsubscribe from the "private-chat" channel
59
+ When I manually trigger the "client-message" event on the "private-chat" channel
60
+ Then I should not receive a "client-message" event on the "private-chat" channel
61
+ And Bob should not receive a "client-message" event on the "private-chat" channel
62
+
63
+ Scenario: Client triggers a client event on an unsubscribed private channel
64
+ Given Bob is subscribed to the "private-chat" channel
65
+ When I manually trigger the "client-message" event on the "private-chat" channel
66
+ Then I should not receive a "client-message" event on the "private-chat" channel
67
+ And Bob should not receive a "client-message" event on the "private-chat" channel
68
+
69
+ Scenario: Client triggers a client event on a subscribed public channel
70
+ Given I am subscribed to the "chat" channel
71
+ And Bob is subscribed to the "chat" channel
72
+ When I trigger the "client-message" event on the "chat" channel
73
+ Then I should not receive a "client-message" event on the "chat" channel
74
+ And Bob should not receive a "client-message" event on the "chat" channel
@@ -0,0 +1,8 @@
1
+ @javascript
2
+ Feature: Client connecting to the server
3
+
4
+ Background:
5
+ Given I am on the homepage
6
+
7
+ Scenario: Client connects to the server
8
+ Then I should be connected
@@ -0,0 +1,82 @@
1
+ Given %{I am subscribed to the "$channel" channel} do |channel|
2
+ step %{I subscribe to the "#{channel}" channel}
3
+ step %{I should be subscribed to the "#{channel}" channel}
4
+ end
5
+
6
+ Given %{$name is subscribed to the "$channel" channel} do |name, channel|
7
+ using_session(name) do
8
+ step %{I am subscribed to the "#{channel}" channel}
9
+ end
10
+ end
11
+
12
+ When %{I subscribe to the "$channel" channel} do |channel|
13
+ page.execute_script("Pusher.instance.subscribe(#{channel.to_json})")
14
+ end
15
+
16
+ When %{I subscribe to the "$channel" channel with presence events} do |channel|
17
+ page.execute_script(%{
18
+ var list = document.querySelector("#presence ul"),
19
+ channel = Pusher.instance.subscribe(#{channel.to_json});
20
+
21
+ channel.bind("pusher:subscription_succeeded", function(clients) {
22
+ var
23
+ count = document.querySelector("#presence header h1 span");
24
+ count.innerHTML = clients.count;
25
+
26
+ clients.each(function(client) {
27
+ var
28
+ element = list.appendChild(document.createElement("li"));
29
+ element.setAttribute("id", "client-" + client.id);
30
+ });
31
+ });
32
+ channel.bind("pusher:member_added", function(client) {
33
+ var
34
+ count = document.querySelector("#presence header h1 span");
35
+ count.innerHTML = parseInt(count.innerHTML, 10) + 1;
36
+
37
+ var
38
+ element = list.appendChild(document.createElement("li"));
39
+ element.setAttribute("id", "client-" + client.id);
40
+ });
41
+ channel.bind("pusher:member_removed", function(client) {
42
+ var item = list.querySelector("li#client-" + client.id),
43
+ count = document.querySelector("#presence header h1 span");
44
+
45
+ count.innerHTML = parseInt(count.innerHTML, 10) - 1;
46
+
47
+ list.removeChild(item);
48
+ });
49
+ })
50
+ end
51
+
52
+ When %{I unsubscribe from the "$channel" channel} do |channel|
53
+ page.execute_script("Pusher.instance.unsubscribe(#{channel.to_json})")
54
+ end
55
+
56
+ When %{$name unsubscribes from the "$channel" channel} do |name, channel|
57
+ using_session(name) do
58
+ step %{I unsubscribe from the "#{channel}" channel}
59
+ end
60
+ end
61
+
62
+ Then %{I should be subscribed to the "$channel" channel} do |channel|
63
+ wait_until do
64
+ subscribed = page.evaluate_script(%{
65
+ var
66
+ channel = Pusher.instance.channel(#{channel.to_json});
67
+ channel && channel.subscribed;
68
+ })
69
+ subscribed == true
70
+ end
71
+ end
72
+
73
+ Then %{I should not be subscribed to the "$channel" channel} do |channel|
74
+ wait do
75
+ subscribed = page.evaluate_script(%{
76
+ var
77
+ channel = Pusher.instance.channel(#{channel.to_json});
78
+ channel && channel.subscribed;
79
+ })
80
+ subscribed.should be_false
81
+ end
82
+ end
@@ -0,0 +1,28 @@
1
+ Given "I am connected" do
2
+ step %{I am on the homepage}
3
+ step %{I should be connected}
4
+ end
5
+
6
+ Given %{$name is connected} do |name|
7
+ using_session(name) do
8
+ step %{I am connected}
9
+ end
10
+ end
11
+
12
+ Given "I change my socket ID" do
13
+ page.execute_script("Pusher.instance.connection.socket_id = -1;")
14
+ end
15
+
16
+ Then "I should be connected" do
17
+ wait_until do
18
+ state = page.evaluate_script("Pusher.instance.connection.state")
19
+ state == "connected"
20
+ end
21
+ end
22
+
23
+ Then "I should not be connected" do
24
+ wait_until do
25
+ state = page.evaluate_script("Pusher.instance.connection.state")
26
+ state == "unavailable"
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ When %{a "$event" event is triggered on the "$channel" channel} do |event, channel|
2
+ Pusher[channel].trigger(event, {})
3
+ end
4
+
5
+ When %{I trigger the "$event" event on the "$channel" channel} do |event, channel|
6
+ page.execute_script(%{
7
+ var
8
+ channel = Pusher.instance.channel(#{channel.to_json});
9
+ channel.trigger(#{event.to_json}, {});
10
+ })
11
+ end
12
+
13
+ When %{I manually trigger the "$event" event on the "$channel" channel} do |event, channel|
14
+ page.execute_script(%{Pusher.instance.send_event(#{event.to_json}, {}, #{channel.to_json})})
15
+ end
16
+
17
+ Then /^I should receive a "([^"]+)" event on the "([^"]+)" channel$/ do |event, channel|
18
+ wait do
19
+ events = page.evaluate_script("Pusher.instance.events[#{[channel, event].join(":").to_json}]")
20
+ events.length.should == 1
21
+ end
22
+ end
23
+
24
+ Then /^I should not receive a "([^"]+)" event on the "([^"]+)" channel$/ do |event, channel|
25
+ wait do
26
+ events = page.evaluate_script("Pusher.instance.events[#{[channel, event].join(":").to_json}]")
27
+ events.should be_nil
28
+ end
29
+ end
30
+
31
+ Then /^([^I]+) should receive a "([^"]+)" event on the "([^"]+)" channel$/ do |name, event, channel|
32
+ using_session(name) do
33
+ step %{I should receive a "#{event}" event on the "#{channel}" channel}
34
+ end
35
+ end
36
+
37
+ Then /^([^I]+) should not receive a "([^"]+)" event on the "([^"]+)" channel$/ do |name, event, channel|
38
+ using_session(name) do
39
+ step %{I should not receive a "#{event}" event on the "#{channel}" channel}
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ Given "I am on the homepage" do
2
+ visit "/"
3
+ end
@@ -0,0 +1,6 @@
1
+ Then /^I should see (\d+) clients?$/ do |count|
2
+ within("#presence") do
3
+ should have_css("header h1 span", text: count)
4
+ should have_css("ul li", count: count.to_i)
5
+ end
6
+ end
@@ -0,0 +1,29 @@
1
+ require "pusher"
2
+ require "sinatra"
3
+
4
+ Pusher.url = "http://PUSHER_API_KEY:PUSHER_API_SECRET@localhost:8081/apps/PUSHER_APP_ID"
5
+
6
+ class Sinatra::Application
7
+ set :root, Proc.new { File.join(File.dirname(__FILE__), "application") }
8
+ set :views, Proc.new { File.join(root, "views") }
9
+ set :public_folder, Proc.new { File.join(root, "public") }
10
+
11
+ disable :logging
12
+
13
+ get "/" do
14
+ erb :index
15
+ end
16
+
17
+ post "/pusher/auth" do
18
+ channel = Pusher[params[:channel_name]]
19
+
20
+ if params[:channel_name] =~ /^private-/
21
+ channel.authenticate(params[:socket_id]).to_json
22
+ elsif params[:channel_name] =~ /^presence-/
23
+ channel.authenticate(params[:socket_id], {
24
+ user_id: params[:socket_id],
25
+ user_info: {}
26
+ }).to_json
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,1155 @@
1
+ /*!
2
+ * Pusher JavaScript Library v1.11.0
3
+ * http://pusherapp.com/
4
+ *
5
+ * Copyright 2011, Pusher
6
+ * Released under the MIT licence.
7
+ */
8
+
9
+ if (Function.prototype.scopedTo === undefined) {
10
+ Function.prototype.scopedTo = function(context, args) {
11
+ var f = this;
12
+ return function() {
13
+ return f.apply(context, Array.prototype.slice.call(args || [])
14
+ .concat(Array.prototype.slice.call(arguments)));
15
+ };
16
+ };
17
+ }
18
+
19
+ var Pusher = function(app_key, options) {
20
+ this.options = options || {};
21
+ this.key = app_key;
22
+ this.channels = new Pusher.Channels();
23
+ this.global_emitter = new Pusher.EventsDispatcher()
24
+
25
+ var self = this;
26
+
27
+ this.checkAppKey();
28
+
29
+ this.connection = new Pusher.Connection(this.key, this.options);
30
+
31
+ // Setup / teardown connection
32
+ this.connection
33
+ .bind('connected', function() {
34
+ self.subscribeAll();
35
+ })
36
+ .bind('message', function(params) {
37
+ var internal = (params.event.indexOf('pusher_internal:') === 0);
38
+ if (params.channel) {
39
+ var channel;
40
+ if (channel = self.channel(params.channel)) {
41
+ channel.emit(params.event, params.data);
42
+ }
43
+ }
44
+ // Emit globaly [deprecated]
45
+ if (!internal) self.global_emitter.emit(params.event, params.data);
46
+ })
47
+ .bind('disconnected', function() {
48
+ self.channels.disconnect();
49
+ })
50
+ .bind('error', function(err) {
51
+ Pusher.warn('Error', err);
52
+ });
53
+
54
+ Pusher.instances.push(this);
55
+
56
+ if (Pusher.isReady) self.connect();
57
+ };
58
+ Pusher.instances = [];
59
+ Pusher.prototype = {
60
+ channel: function(name) {
61
+ return this.channels.find(name);
62
+ },
63
+
64
+ connect: function() {
65
+ this.connection.connect();
66
+ },
67
+
68
+ disconnect: function() {
69
+ this.connection.disconnect();
70
+ },
71
+
72
+ bind: function(event_name, callback) {
73
+ this.global_emitter.bind(event_name, callback);
74
+ return this;
75
+ },
76
+
77
+ bind_all: function(callback) {
78
+ this.global_emitter.bind_all(callback);
79
+ return this;
80
+ },
81
+
82
+ subscribeAll: function() {
83
+ var channel;
84
+ for (channel in this.channels.channels) {
85
+ if (this.channels.channels.hasOwnProperty(channel)) {
86
+ this.subscribe(channel);
87
+ }
88
+ }
89
+ },
90
+
91
+ subscribe: function(channel_name) {
92
+ var self = this;
93
+ var channel = this.channels.add(channel_name, this);
94
+ if (this.connection.state === 'connected') {
95
+ channel.authorize(this, function(err, data) {
96
+ if (err) {
97
+ channel.emit('pusher:subscription_error', data);
98
+ } else {
99
+ self.send_event('pusher:subscribe', {
100
+ channel: channel_name,
101
+ auth: data.auth,
102
+ channel_data: data.channel_data
103
+ });
104
+ }
105
+ });
106
+ }
107
+ return channel;
108
+ },
109
+
110
+ unsubscribe: function(channel_name) {
111
+ this.channels.remove(channel_name);
112
+ if (this.connection.state === 'connected') {
113
+ this.send_event('pusher:unsubscribe', {
114
+ channel: channel_name
115
+ });
116
+ }
117
+ },
118
+
119
+ send_event: function(event_name, data, channel) {
120
+ return this.connection.send_event(event_name, data, channel);
121
+ },
122
+
123
+ checkAppKey: function() {
124
+ if(this.key === null || this.key === undefined) {
125
+ Pusher.warn('Warning', 'You must pass your app key when you instantiate Pusher.');
126
+ }
127
+ }
128
+ };
129
+
130
+ Pusher.Util = {
131
+ extend: function extend(target, extensions) {
132
+ for (var property in extensions) {
133
+ if (extensions[property] && extensions[property].constructor &&
134
+ extensions[property].constructor === Object) {
135
+ target[property] = extend(target[property] || {}, extensions[property]);
136
+ } else {
137
+ target[property] = extensions[property];
138
+ }
139
+ }
140
+ return target;
141
+ },
142
+
143
+ stringify: function stringify() {
144
+ var m = ["Pusher"]
145
+ for (var i = 0; i < arguments.length; i++){
146
+ if (typeof arguments[i] === "string") {
147
+ m.push(arguments[i])
148
+ } else {
149
+ if (window['JSON'] == undefined) {
150
+ m.push(arguments[i].toString());
151
+ } else {
152
+ m.push(JSON.stringify(arguments[i]))
153
+ }
154
+ }
155
+ };
156
+ return m.join(" : ")
157
+ },
158
+
159
+ arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf
160
+ var nativeIndexOf = Array.prototype.indexOf;
161
+ if (array == null) return -1;
162
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
163
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
164
+ return -1;
165
+ }
166
+ };
167
+
168
+ // To receive log output provide a Pusher.log function, for example
169
+ // Pusher.log = function(m){console.log(m)}
170
+ Pusher.debug = function() {
171
+ if (!Pusher.log) return
172
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments))
173
+ }
174
+ Pusher.warn = function() {
175
+ if (window.console && window.console.warn) {
176
+ window.console.warn(Pusher.Util.stringify.apply(this, arguments));
177
+ } else {
178
+ if (!Pusher.log) return
179
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments));
180
+ }
181
+ };
182
+
183
+ // Pusher defaults
184
+ Pusher.VERSION = '1.11.0';
185
+
186
+ Pusher.host = 'ws.pusherapp.com';
187
+ Pusher.ws_port = 80;
188
+ Pusher.wss_port = 443;
189
+ Pusher.channel_auth_endpoint = '/pusher/auth';
190
+ Pusher.cdn_http = 'http://js.pusher.com/'
191
+ Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'
192
+ Pusher.dependency_suffix = '';
193
+ Pusher.channel_auth_transport = 'ajax';
194
+ Pusher.activity_timeout = 120000;
195
+ Pusher.pong_timeout = 30000;
196
+
197
+ Pusher.isReady = false;
198
+ Pusher.ready = function() {
199
+ Pusher.isReady = true;
200
+ for (var i = 0, l = Pusher.instances.length; i < l; i++) {
201
+ Pusher.instances[i].connect();
202
+ }
203
+ };
204
+
205
+ ;(function() {
206
+ /* Abstract event binding
207
+ Example:
208
+
209
+ var MyEventEmitter = function(){};
210
+ MyEventEmitter.prototype = new Pusher.EventsDispatcher;
211
+
212
+ var emitter = new MyEventEmitter();
213
+
214
+ // Bind to single event
215
+ emitter.bind('foo_event', function(data){ alert(data)} );
216
+
217
+ // Bind to all
218
+ emitter.bind_all(function(event_name, data){ alert(data) });
219
+
220
+ --------------------------------------------------------*/
221
+ function EventsDispatcher(failThrough) {
222
+ this.callbacks = {};
223
+ this.global_callbacks = [];
224
+ // Run this function when dispatching an event when no callbacks defined
225
+ this.failThrough = failThrough;
226
+ }
227
+
228
+ EventsDispatcher.prototype.bind = function(event_name, callback) {
229
+ this.callbacks[event_name] = this.callbacks[event_name] || [];
230
+ this.callbacks[event_name].push(callback);
231
+ return this;// chainable
232
+ };
233
+
234
+ EventsDispatcher.prototype.unbind = function(eventName, callback) {
235
+ if(this.callbacks[eventName]) {
236
+ var index = Pusher.Util.arrayIndexOf(this.callbacks[eventName], callback);
237
+ this.callbacks[eventName].splice(index, 1);
238
+ }
239
+ return this;
240
+ };
241
+
242
+ EventsDispatcher.prototype.emit = function(event_name, data) {
243
+ // Global callbacks
244
+ for (var i = 0; i < this.global_callbacks.length; i++) {
245
+ this.global_callbacks[i](event_name, data);
246
+ }
247
+
248
+ // Event callbacks
249
+ var callbacks = this.callbacks[event_name];
250
+ if (callbacks) {
251
+ for (var i = 0; i < callbacks.length; i++) {
252
+ callbacks[i](data);
253
+ }
254
+ } else if (this.failThrough) {
255
+ this.failThrough(event_name, data)
256
+ }
257
+
258
+ return this;
259
+ };
260
+
261
+ EventsDispatcher.prototype.bind_all = function(callback) {
262
+ this.global_callbacks.push(callback);
263
+ return this;
264
+ };
265
+
266
+ this.Pusher.EventsDispatcher = EventsDispatcher;
267
+ }).call(this);
268
+
269
+ ;(function() {
270
+ var Pusher = this.Pusher;
271
+
272
+ /*-----------------------------------------------
273
+ Helpers:
274
+ -----------------------------------------------*/
275
+
276
+ function capitalize(str) {
277
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
278
+ }
279
+
280
+
281
+ function safeCall(method, obj, data) {
282
+ if (obj[method] !== undefined) {
283
+ obj[method](data);
284
+ }
285
+ }
286
+
287
+ /*-----------------------------------------------
288
+ The State Machine
289
+ -----------------------------------------------*/
290
+ function Machine(initialState, transitions, stateActions) {
291
+ Pusher.EventsDispatcher.call(this);
292
+
293
+ this.state = undefined;
294
+ this.errors = [];
295
+
296
+ // functions for each state
297
+ this.stateActions = stateActions;
298
+
299
+ // set up the transitions
300
+ this.transitions = transitions;
301
+
302
+ this.transition(initialState);
303
+ };
304
+
305
+ Machine.prototype.transition = function(nextState, data) {
306
+ var prevState = this.state;
307
+ var stateCallbacks = this.stateActions;
308
+
309
+ if (prevState && (Pusher.Util.arrayIndexOf(this.transitions[prevState], nextState) == -1)) {
310
+ throw new Error('Invalid transition [' + prevState + ' to ' + nextState + ']');
311
+ }
312
+
313
+ // exit
314
+ safeCall(prevState + 'Exit', stateCallbacks, data);
315
+
316
+ // tween
317
+ safeCall(prevState + 'To' + capitalize(nextState), stateCallbacks, data);
318
+
319
+ // pre
320
+ safeCall(nextState + 'Pre', stateCallbacks, data);
321
+
322
+ // change state:
323
+ this.state = nextState;
324
+
325
+ // handy to bind to
326
+ this.emit('state_change', {
327
+ oldState: prevState,
328
+ newState: nextState
329
+ });
330
+
331
+ // Post:
332
+ safeCall(nextState + 'Post', stateCallbacks, data);
333
+ };
334
+
335
+ Machine.prototype.is = function(state) {
336
+ return this.state === state;
337
+ };
338
+
339
+ Machine.prototype.isNot = function(state) {
340
+ return this.state !== state;
341
+ };
342
+
343
+ Pusher.Util.extend(Machine.prototype, Pusher.EventsDispatcher.prototype);
344
+
345
+ this.Pusher.Machine = Machine;
346
+ }).call(this);
347
+
348
+ ;(function() {
349
+ /*
350
+ A little bauble to interface with window.navigator.onLine,
351
+ window.ononline and window.onoffline. Easier to mock.
352
+ */
353
+ var NetInfo = function() {
354
+ var self = this;
355
+ Pusher.EventsDispatcher.call(this);
356
+ // This is okay, as IE doesn't support this stuff anyway.
357
+ if (window.addEventListener !== undefined) {
358
+ window.addEventListener("online", function() {
359
+ self.emit('online', null);
360
+ }, false);
361
+ window.addEventListener("offline", function() {
362
+ self.emit('offline', null);
363
+ }, false);
364
+ }
365
+ };
366
+
367
+ // Offline means definitely offline (no connection to router).
368
+ // Inverse does NOT mean definitely online (only currently supported in Safari
369
+ // and even there only means the device has a connection to the router).
370
+ NetInfo.prototype.isOnLine = function() {
371
+ if (window.navigator.onLine === undefined) {
372
+ return true;
373
+ } else {
374
+ return window.navigator.onLine;
375
+ }
376
+ };
377
+
378
+ Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
379
+
380
+ this.Pusher.NetInfo = NetInfo;
381
+ }).call(this);
382
+
383
+ ;(function() {
384
+ var Pusher = this.Pusher;
385
+
386
+ var machineTransitions = {
387
+ 'initialized': ['waiting', 'failed'],
388
+ 'waiting': ['connecting', 'permanentlyClosed'],
389
+ 'connecting': ['open', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
390
+ 'open': ['connected', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
391
+ 'connected': ['permanentlyClosing', 'impermanentlyClosing', 'waiting'],
392
+ 'impermanentlyClosing': ['waiting', 'permanentlyClosing'],
393
+ 'permanentlyClosing': ['permanentlyClosed'],
394
+ 'permanentlyClosed': ['waiting'],
395
+ 'failed': ['permanentlyClosing']
396
+ };
397
+
398
+
399
+ // Amount to add to time between connection attemtpts per failed attempt.
400
+ var UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT = 2000;
401
+ var UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
402
+ var UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
403
+
404
+ var MAX_CONNECTION_ATTEMPT_WAIT = 5 * UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
405
+ var MAX_OPEN_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
406
+ var MAX_CONNECTED_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
407
+
408
+ function resetConnectionParameters(connection) {
409
+ connection.connectionWait = 0;
410
+
411
+ if (Pusher.TransportType === 'flash') {
412
+ // Flash needs a bit more time
413
+ connection.openTimeout = 5000;
414
+ } else {
415
+ connection.openTimeout = 2000;
416
+ }
417
+ connection.connectedTimeout = 2000;
418
+ connection.connectionSecure = connection.compulsorySecure;
419
+ connection.connectionAttempts = 0;
420
+ }
421
+
422
+ function Connection(key, options) {
423
+ var self = this;
424
+
425
+ Pusher.EventsDispatcher.call(this);
426
+
427
+ this.options = Pusher.Util.extend({encrypted: false}, options);
428
+
429
+ this.netInfo = new Pusher.NetInfo();
430
+
431
+ this.netInfo.bind('online', function(){
432
+ if (self._machine.is('waiting')) {
433
+ self._machine.transition('connecting');
434
+ triggerStateChange('connecting');
435
+ }
436
+ });
437
+
438
+ this.netInfo.bind('offline', function() {
439
+ if (self._machine.is('connected')) {
440
+ // These are for Chrome 15, which ends up
441
+ // having two sockets hanging around.
442
+ self.socket.onclose = undefined;
443
+ self.socket.onmessage = undefined;
444
+ self.socket.onerror = undefined;
445
+ self.socket.onopen = undefined;
446
+
447
+ self.socket.close();
448
+ self.socket = undefined;
449
+ self._machine.transition('waiting');
450
+ }
451
+ });
452
+
453
+ // define the state machine that runs the connection
454
+ this._machine = new Pusher.Machine('initialized', machineTransitions, {
455
+
456
+ // TODO: Use the constructor for this.
457
+ initializedPre: function() {
458
+ self.compulsorySecure = self.options.encrypted;
459
+
460
+ self.key = key;
461
+ self.socket = null;
462
+ self.socket_id = null;
463
+
464
+ self.state = 'initialized';
465
+ },
466
+
467
+ waitingPre: function() {
468
+ if (self.connectionWait > 0) {
469
+ self.emit('connecting_in', self.connectionWait);
470
+ }
471
+
472
+ if (self.netInfo.isOnLine() === false || self.connectionAttempts > 4){
473
+ triggerStateChange('unavailable');
474
+ } else {
475
+ triggerStateChange('connecting');
476
+ }
477
+
478
+ if (self.netInfo.isOnLine() === true) {
479
+ self._waitingTimer = setTimeout(function() {
480
+ self._machine.transition('connecting');
481
+ }, self.connectionWait);
482
+ }
483
+ },
484
+
485
+ waitingExit: function() {
486
+ clearTimeout(self._waitingTimer);
487
+ },
488
+
489
+ connectingPre: function() {
490
+ // Case that a user manages to get to the connecting
491
+ // state even when offline.
492
+ if (self.netInfo.isOnLine() === false) {
493
+ self._machine.transition('waiting');
494
+ triggerStateChange('unavailable');
495
+
496
+ return;
497
+ }
498
+
499
+ // removed: if not closed, something is wrong that we should fix
500
+ // if(self.socket !== undefined) self.socket.close();
501
+ var url = formatURL(self.key, self.connectionSecure);
502
+ Pusher.debug('Connecting', url);
503
+ self.socket = new Pusher.Transport(url);
504
+ // now that the socket connection attempt has been started,
505
+ // set up the callbacks fired by the socket for different outcomes
506
+ self.socket.onopen = ws_onopen;
507
+ self.socket.onclose = transitionToWaiting;
508
+ self.socket.onerror = ws_onError;
509
+
510
+ // allow time to get ws_onOpen, otherwise close socket and try again
511
+ self._connectingTimer = setTimeout(TransitionToImpermanentClosing, self.openTimeout);
512
+ },
513
+
514
+ connectingExit: function() {
515
+ clearTimeout(self._connectingTimer);
516
+ },
517
+
518
+ connectingToWaiting: function() {
519
+ updateConnectionParameters();
520
+
521
+ // FUTURE: update only ssl
522
+ },
523
+
524
+ connectingToImpermanentlyClosing: function() {
525
+ updateConnectionParameters();
526
+
527
+ // FUTURE: update only timeout
528
+ },
529
+
530
+ openPre: function() {
531
+ self.socket.onmessage = ws_onMessageOpen;
532
+ self.socket.onerror = ws_onError;
533
+ self.socket.onclose = transitionToWaiting;
534
+
535
+ // allow time to get connected-to-Pusher message, otherwise close socket, try again
536
+ self._openTimer = setTimeout(TransitionToImpermanentClosing, self.connectedTimeout);
537
+ },
538
+
539
+ openExit: function() {
540
+ clearTimeout(self._openTimer);
541
+ },
542
+
543
+ openToWaiting: function() {
544
+ updateConnectionParameters();
545
+ },
546
+
547
+ openToImpermanentlyClosing: function() {
548
+ updateConnectionParameters();
549
+ },
550
+
551
+ connectedPre: function(socket_id) {
552
+ self.socket_id = socket_id;
553
+
554
+ self.socket.onmessage = ws_onMessageConnected;
555
+ self.socket.onerror = ws_onError;
556
+ self.socket.onclose = transitionToWaiting;
557
+
558
+ resetConnectionParameters(self);
559
+
560
+ resetActivityCheck();
561
+ },
562
+
563
+ connectedPost: function() {
564
+ triggerStateChange('connected');
565
+ },
566
+
567
+ connectedExit: function() {
568
+ stopActivityCheck();
569
+ triggerStateChange('disconnected');
570
+ },
571
+
572
+ impermanentlyClosingPost: function() {
573
+ if (self.socket) {
574
+ self.socket.onclose = transitionToWaiting;
575
+ self.socket.close();
576
+ }
577
+ },
578
+
579
+ permanentlyClosingPost: function() {
580
+ if (self.socket) {
581
+ self.socket.onclose = function() {
582
+ resetConnectionParameters(self);
583
+ self._machine.transition('permanentlyClosed');
584
+ };
585
+
586
+ self.socket.close();
587
+ } else {
588
+ resetConnectionParameters(self);
589
+ self._machine.transition('permanentlyClosed');
590
+ }
591
+ },
592
+
593
+ failedPre: function() {
594
+ triggerStateChange('failed');
595
+ Pusher.debug('WebSockets are not available in this browser.');
596
+ }
597
+ });
598
+
599
+ /*-----------------------------------------------
600
+ -----------------------------------------------*/
601
+
602
+ function updateConnectionParameters() {
603
+ if (self.connectionWait < MAX_CONNECTION_ATTEMPT_WAIT) {
604
+ self.connectionWait += UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
605
+ }
606
+
607
+ if (self.openTimeout < MAX_OPEN_ATTEMPT_TIMEOUT) {
608
+ self.openTimeout += UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
609
+ }
610
+
611
+ if (self.connectedTimeout < MAX_CONNECTED_ATTEMPT_TIMEOUT) {
612
+ self.connectedTimeout += UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
613
+ }
614
+
615
+ if (self.compulsorySecure !== true) {
616
+ self.connectionSecure = !self.connectionSecure;
617
+ }
618
+
619
+ self.connectionAttempts++;
620
+ }
621
+
622
+ function formatURL(key, isSecure) {
623
+ var port = Pusher.ws_port;
624
+ var protocol = 'ws://';
625
+
626
+ // Always connect with SSL if the current page has
627
+ // been loaded via HTTPS.
628
+ //
629
+ // FUTURE: Always connect using SSL.
630
+ //
631
+ if (isSecure || document.location.protocol === 'https:') {
632
+ port = Pusher.wss_port;
633
+ protocol = 'wss://';
634
+ }
635
+
636
+ return protocol + Pusher.host + ':' + port + '/app/' + key + '?client=js&version=' + Pusher.VERSION;
637
+ }
638
+
639
+ // callback for close and retry. Used on timeouts.
640
+ function TransitionToImpermanentClosing() {
641
+ self._machine.transition('impermanentlyClosing');
642
+ }
643
+
644
+ function resetActivityCheck() {
645
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
646
+ // Send ping after inactivity
647
+ self._activityTimer = setTimeout(function() {
648
+ self.send_event('pusher:ping', {})
649
+ // Wait for pong response
650
+ self._activityTimer = setTimeout(function() {
651
+ self.socket.close();
652
+ }, (self.options.pong_timeout || Pusher.pong_timeout))
653
+ }, (self.options.activity_timeout || Pusher.activity_timeout))
654
+ }
655
+
656
+ function stopActivityCheck() {
657
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
658
+ }
659
+
660
+ /*-----------------------------------------------
661
+ WebSocket Callbacks
662
+ -----------------------------------------------*/
663
+
664
+ // no-op, as we only care when we get pusher:connection_established
665
+ function ws_onopen() {
666
+ self._machine.transition('open');
667
+ };
668
+
669
+ function ws_onMessageOpen(event) {
670
+ var params = parseWebSocketEvent(event);
671
+ if (params !== undefined) {
672
+ if (params.event === 'pusher:connection_established') {
673
+ self._machine.transition('connected', params.data.socket_id);
674
+ } else if (params.event === 'pusher:error') {
675
+ // first inform the end-developer of this error
676
+ self.emit('error', {type: 'PusherError', data: params.data});
677
+
678
+ switch (params.data.code) {
679
+ case 4000:
680
+ Pusher.warn(params.data.message);
681
+
682
+ self.compulsorySecure = true;
683
+ self.connectionSecure = true;
684
+ self.options.encrypted = true;
685
+ break;
686
+ case 4001:
687
+ // App not found by key - close connection
688
+ self._machine.transition('permanentlyClosing');
689
+ break;
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ function ws_onMessageConnected(event) {
696
+ resetActivityCheck();
697
+
698
+ var params = parseWebSocketEvent(event);
699
+ if (params !== undefined) {
700
+ Pusher.debug('Event recd', params);
701
+
702
+ switch (params.event) {
703
+ case 'pusher:error':
704
+ self.emit('error', {type: 'PusherError', data: params.data});
705
+ break;
706
+ case 'pusher:ping':
707
+ self.send_event('pusher:pong', {})
708
+ break;
709
+ }
710
+
711
+ self.emit('message', params);
712
+ }
713
+ }
714
+
715
+
716
+ /**
717
+ * Parses an event from the WebSocket to get
718
+ * the JSON payload that we require
719
+ *
720
+ * @param {MessageEvent} event The event from the WebSocket.onmessage handler.
721
+ **/
722
+ function parseWebSocketEvent(event) {
723
+ try {
724
+ var params = JSON.parse(event.data);
725
+
726
+ if (typeof params.data === 'string') {
727
+ try {
728
+ params.data = JSON.parse(params.data);
729
+ } catch (e) {
730
+ if (!(e instanceof SyntaxError)) {
731
+ throw e;
732
+ }
733
+ }
734
+ }
735
+
736
+ return params;
737
+ } catch (e) {
738
+ self.emit('error', {type: 'MessageParseError', error: e, data: event.data});
739
+ }
740
+ }
741
+
742
+ function transitionToWaiting() {
743
+ self._machine.transition('waiting');
744
+ }
745
+
746
+ function ws_onError() {
747
+ self.emit('error', {
748
+ type: 'WebSocketError'
749
+ });
750
+
751
+ // note: required? is the socket auto closed in the case of error?
752
+ self.socket.close();
753
+ self._machine.transition('impermanentlyClosing');
754
+ }
755
+
756
+ function triggerStateChange(newState, data) {
757
+ // avoid emitting and changing the state
758
+ // multiple times when it's the same.
759
+ if (self.state === newState) return;
760
+
761
+ var prevState = self.state;
762
+
763
+ self.state = newState;
764
+
765
+ Pusher.debug('State changed', prevState + ' -> ' + newState);
766
+
767
+ self.emit('state_change', {previous: prevState, current: newState});
768
+ self.emit(newState, data);
769
+ }
770
+ };
771
+
772
+ Connection.prototype.connect = function() {
773
+ // no WebSockets
774
+ if (Pusher.Transport === null || Pusher.Transport === undefined) {
775
+ this._machine.transition('failed');
776
+ }
777
+ // initial open of connection
778
+ else if(this._machine.is('initialized')) {
779
+ resetConnectionParameters(this);
780
+ this._machine.transition('waiting');
781
+ }
782
+ // user skipping connection wait
783
+ else if (this._machine.is('waiting') && this.netInfo.isOnLine() === true) {
784
+ this._machine.transition('connecting');
785
+ }
786
+ // user re-opening connection after closing it
787
+ else if(this._machine.is("permanentlyClosed")) {
788
+ this._machine.transition('waiting');
789
+ }
790
+ };
791
+
792
+ Connection.prototype.send = function(data) {
793
+ if (this._machine.is('connected')) {
794
+ this.socket.send(data);
795
+ return true;
796
+ } else {
797
+ return false;
798
+ }
799
+ };
800
+
801
+ Connection.prototype.send_event = function(event_name, data, channel) {
802
+ var payload = {
803
+ event: event_name,
804
+ data: data
805
+ };
806
+ if (channel) payload['channel'] = channel;
807
+
808
+ Pusher.debug('Event sent', payload);
809
+ return this.send(JSON.stringify(payload));
810
+ }
811
+
812
+ Connection.prototype.disconnect = function() {
813
+ if (this._machine.is('permanentlyClosed')) return;
814
+
815
+ if (this._machine.is('waiting')) {
816
+ this._machine.transition('permanentlyClosed');
817
+ } else {
818
+ this._machine.transition('permanentlyClosing');
819
+ }
820
+ };
821
+
822
+ Pusher.Util.extend(Connection.prototype, Pusher.EventsDispatcher.prototype);
823
+ this.Pusher.Connection = Connection;
824
+ }).call(this);
825
+
826
+ Pusher.Channels = function() {
827
+ this.channels = {};
828
+ };
829
+
830
+ Pusher.Channels.prototype = {
831
+ add: function(channel_name, pusher) {
832
+ var existing_channel = this.find(channel_name);
833
+ if (!existing_channel) {
834
+ var channel = Pusher.Channel.factory(channel_name, pusher);
835
+ this.channels[channel_name] = channel;
836
+ return channel;
837
+ } else {
838
+ return existing_channel;
839
+ }
840
+ },
841
+
842
+ find: function(channel_name) {
843
+ return this.channels[channel_name];
844
+ },
845
+
846
+ remove: function(channel_name) {
847
+ delete this.channels[channel_name];
848
+ },
849
+
850
+ disconnect: function () {
851
+ for(var channel_name in this.channels){
852
+ this.channels[channel_name].disconnect()
853
+ }
854
+ }
855
+ };
856
+
857
+ Pusher.Channel = function(channel_name, pusher) {
858
+ var self = this;
859
+ Pusher.EventsDispatcher.call(this, function(event_name, event_data) {
860
+ Pusher.debug('No callbacks on ' + channel_name + ' for ' + event_name);
861
+ });
862
+
863
+ this.pusher = pusher;
864
+ this.name = channel_name;
865
+ this.subscribed = false;
866
+
867
+ this.bind('pusher_internal:subscription_succeeded', function(data) {
868
+ self.onSubscriptionSucceeded(data);
869
+ });
870
+ };
871
+
872
+ Pusher.Channel.prototype = {
873
+ // inheritable constructor
874
+ init: function() {},
875
+ disconnect: function() {},
876
+
877
+ onSubscriptionSucceeded: function(data) {
878
+ this.subscribed = true;
879
+ this.emit('pusher:subscription_succeeded');
880
+ },
881
+
882
+ authorize: function(pusher, callback){
883
+ callback(false, {}); // normal channels don't require auth
884
+ },
885
+
886
+ trigger: function(event, data) {
887
+ return this.pusher.send_event(event, data, this.name);
888
+ }
889
+ };
890
+
891
+ Pusher.Util.extend(Pusher.Channel.prototype, Pusher.EventsDispatcher.prototype);
892
+
893
+
894
+
895
+ Pusher.auth_callbacks = {};
896
+
897
+ Pusher.authorizers = {
898
+ ajax: function(pusher, callback){
899
+ var self = this, xhr;
900
+
901
+ if (Pusher.XHR) {
902
+ xhr = new Pusher.XHR();
903
+ } else {
904
+ xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
905
+ }
906
+
907
+ xhr.open("POST", Pusher.channel_auth_endpoint, true);
908
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
909
+ xhr.onreadystatechange = function() {
910
+ if (xhr.readyState == 4) {
911
+ if (xhr.status == 200) {
912
+ var data, parsed = false;
913
+
914
+ try {
915
+ data = JSON.parse(xhr.responseText);
916
+ parsed = true;
917
+ } catch (e) {
918
+ callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText);
919
+ }
920
+
921
+ if (parsed) { // prevents double execution.
922
+ callback(false, data);
923
+ }
924
+ } else {
925
+ Pusher.warn("Couldn't get auth info from your webapp", status);
926
+ callback(true, xhr.status);
927
+ }
928
+ }
929
+ };
930
+ xhr.send('socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(self.name));
931
+ },
932
+ jsonp: function(pusher, callback){
933
+ var qstring = 'socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(this.name);
934
+ var script = document.createElement("script");
935
+ // Hacked wrapper.
936
+ Pusher.auth_callbacks[this.name] = function(data) {
937
+ callback(false, data);
938
+ };
939
+ var callback_name = "Pusher.auth_callbacks['" + this.name + "']";
940
+ script.src = Pusher.channel_auth_endpoint+'?callback='+encodeURIComponent(callback_name)+'&'+qstring;
941
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
942
+ head.insertBefore( script, head.firstChild );
943
+ }
944
+ };
945
+
946
+ Pusher.Channel.PrivateChannel = {
947
+ authorize: function(pusher, callback){
948
+ Pusher.authorizers[Pusher.channel_auth_transport].scopedTo(this)(pusher, callback);
949
+ }
950
+ };
951
+
952
+ Pusher.Channel.PresenceChannel = {
953
+ init: function(){
954
+ this.bind('pusher_internal:member_added', function(data){
955
+ var member = this.members.add(data.user_id, data.user_info);
956
+ this.emit('pusher:member_added', member);
957
+ }.scopedTo(this))
958
+
959
+ this.bind('pusher_internal:member_removed', function(data){
960
+ var member = this.members.remove(data.user_id);
961
+ if (member) {
962
+ this.emit('pusher:member_removed', member);
963
+ }
964
+ }.scopedTo(this))
965
+ },
966
+
967
+ disconnect: function(){
968
+ this.members.clear();
969
+ },
970
+
971
+ onSubscriptionSucceeded: function(data) {
972
+ this.members._members_map = data.presence.hash;
973
+ this.members.count = data.presence.count;
974
+ this.subscribed = true;
975
+
976
+ this.emit('pusher:subscription_succeeded', this.members);
977
+ },
978
+
979
+ members: {
980
+ _members_map: {},
981
+ count: 0,
982
+
983
+ each: function(callback) {
984
+ for(var i in this._members_map) {
985
+ callback({
986
+ id: i,
987
+ info: this._members_map[i]
988
+ });
989
+ }
990
+ },
991
+
992
+ add: function(id, info) {
993
+ this._members_map[id] = info;
994
+ this.count++;
995
+ return this.get(id);
996
+ },
997
+
998
+ remove: function(user_id) {
999
+ var member = this.get(user_id);
1000
+ if (member) {
1001
+ delete this._members_map[user_id];
1002
+ this.count--;
1003
+ }
1004
+ return member;
1005
+ },
1006
+
1007
+ get: function(user_id) {
1008
+ if (this._members_map.hasOwnProperty(user_id)) { // have heard of this user user_id
1009
+ return {
1010
+ id: user_id,
1011
+ info: this._members_map[user_id]
1012
+ }
1013
+ } else { // have never heard of this user
1014
+ return null;
1015
+ }
1016
+ },
1017
+
1018
+ clear: function() {
1019
+ this._members_map = {};
1020
+ this.count = 0;
1021
+ }
1022
+ }
1023
+ };
1024
+
1025
+ Pusher.Channel.factory = function(channel_name, pusher){
1026
+ var channel = new Pusher.Channel(channel_name, pusher);
1027
+ if (channel_name.indexOf('private-') === 0) {
1028
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
1029
+ } else if (channel_name.indexOf('presence-') === 0) {
1030
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
1031
+ Pusher.Util.extend(channel, Pusher.Channel.PresenceChannel);
1032
+ };
1033
+ channel.init();
1034
+ return channel;
1035
+ };
1036
+
1037
+ var _require = (function () {
1038
+
1039
+ var handleScriptLoaded;
1040
+ if (document.addEventListener) {
1041
+ handleScriptLoaded = function (elem, callback) {
1042
+ elem.addEventListener('load', callback, false)
1043
+ }
1044
+ } else {
1045
+ handleScriptLoaded = function(elem, callback) {
1046
+ elem.attachEvent('onreadystatechange', function () {
1047
+ if(elem.readyState == 'loaded' || elem.readyState == 'complete') callback()
1048
+ })
1049
+ }
1050
+ }
1051
+
1052
+ return function (deps, callback) {
1053
+ var dep_count = 0,
1054
+ dep_length = deps.length;
1055
+
1056
+ function checkReady (callback) {
1057
+ dep_count++;
1058
+ if ( dep_length == dep_count ) {
1059
+ // Opera needs the timeout for page initialization weirdness
1060
+ setTimeout(callback, 0);
1061
+ }
1062
+ }
1063
+
1064
+ function addScript (src, callback) {
1065
+ callback = callback || function(){}
1066
+ var head = document.getElementsByTagName('head')[0];
1067
+ var script = document.createElement('script');
1068
+ script.setAttribute('src', src);
1069
+ script.setAttribute("type","text/javascript");
1070
+ script.setAttribute('async', true);
1071
+
1072
+ handleScriptLoaded(script, function () {
1073
+ checkReady(callback);
1074
+ });
1075
+
1076
+ head.appendChild(script);
1077
+ }
1078
+
1079
+ for(var i = 0; i < dep_length; i++) {
1080
+ addScript(deps[i], callback);
1081
+ }
1082
+ }
1083
+ })();
1084
+
1085
+ ;(function() {
1086
+ var cdn = (document.location.protocol == 'http:') ? Pusher.cdn_http : Pusher.cdn_https;
1087
+ var root = cdn + Pusher.VERSION;
1088
+ var deps = [];
1089
+
1090
+ if (window['JSON'] === undefined) {
1091
+ deps.push(root + '/json2' + Pusher.dependency_suffix + '.js');
1092
+ }
1093
+ if (window['WebSocket'] === undefined && window['MozWebSocket'] === undefined) {
1094
+ // We manually initialize web-socket-js to iron out cross browser issues
1095
+ window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
1096
+ deps.push(root + '/flashfallback' + Pusher.dependency_suffix + '.js');
1097
+ }
1098
+
1099
+ var initialize = function() {
1100
+ if (window['WebSocket'] === undefined && window['MozWebSocket'] === undefined) {
1101
+ return function() {
1102
+ // This runs after flashfallback.js has loaded
1103
+ if (window['WebSocket'] !== undefined && window['MozWebSocket'] === undefined) {
1104
+ // window['WebSocket'] is a flash emulation of WebSocket
1105
+ Pusher.Transport = window['WebSocket'];
1106
+ Pusher.TransportType = 'flash';
1107
+
1108
+ window.WEB_SOCKET_SWF_LOCATION = root + "/WebSocketMain.swf";
1109
+ WebSocket.__addTask(function() {
1110
+ Pusher.ready();
1111
+ })
1112
+ WebSocket.__initialize();
1113
+ } else {
1114
+ // Flash must not be installed
1115
+ Pusher.Transport = null;
1116
+ Pusher.TransportType = 'none';
1117
+ Pusher.ready();
1118
+ }
1119
+ }
1120
+ } else {
1121
+ return function() {
1122
+ // This is because Mozilla have decided to
1123
+ // prefix the WebSocket constructor with "Moz".
1124
+ if (window['MozWebSocket'] !== undefined) {
1125
+ Pusher.Transport = window['MozWebSocket'];
1126
+ } else {
1127
+ Pusher.Transport = window['WebSocket'];
1128
+ }
1129
+ // We have some form of a native websocket,
1130
+ // even if the constructor is prefixed:
1131
+ Pusher.TransportType = 'native';
1132
+
1133
+ // Initialise Pusher.
1134
+ Pusher.ready();
1135
+ }
1136
+ }
1137
+ }();
1138
+
1139
+ var ondocumentbody = function(callback) {
1140
+ var load_body = function() {
1141
+ document.body ? callback() : setTimeout(load_body, 0);
1142
+ }
1143
+ load_body();
1144
+ };
1145
+
1146
+ var initializeOnDocumentBody = function() {
1147
+ ondocumentbody(initialize);
1148
+ }
1149
+
1150
+ if (deps.length > 0) {
1151
+ _require(deps, initializeOnDocumentBody);
1152
+ } else {
1153
+ initializeOnDocumentBody();
1154
+ }
1155
+ })();