faye 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of faye might be problematic. Click here for more details.

data/History.txt ADDED
@@ -0,0 +1 @@
1
+
data/Jakefile ADDED
@@ -0,0 +1,2 @@
1
+ require File.join('lib', 'faye')
2
+
data/Manifest.txt ADDED
@@ -0,0 +1,33 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ jake.yml
6
+ Jakefile
7
+ client/util/class.js
8
+ client/util/event.js
9
+ client/util/json.js
10
+ client/util/uri.js
11
+ client/util/xhr.js
12
+ client/faye.js
13
+ client/transport.js
14
+ client/client.js
15
+ client/channel.js
16
+ lib/faye-min.js
17
+ lib/faye.rb
18
+ lib/faye/channel.rb
19
+ lib/faye/connection.rb
20
+ lib/faye/error.rb
21
+ lib/faye/grammar.rb
22
+ lib/faye/rack_adapter.rb
23
+ lib/faye/server.rb
24
+ examples/soapbox/README
25
+ examples/soapbox/app.rb
26
+ examples/soapbox/config.ru
27
+ examples/soapbox/public/jquery.js
28
+ examples/soapbox/public/soapbox.js
29
+ examples/soapbox/public/style.css
30
+ examples/soapbox/views/index.erb
31
+ test/test_channel.rb
32
+ test/test_grammar.rb
33
+ test/test_server.rb
data/README.txt ADDED
@@ -0,0 +1,111 @@
1
+ = Faye
2
+
3
+ * http://github.com/jcoglan/faye
4
+
5
+ Faye is an implementation of the Bayeux protocol in Ruby. It includes a
6
+ server backend and an adapter for Rack, and a self-contained JavaScript
7
+ client. Bayeux is a protocol designed to facilitate Comet, also known as
8
+ 'server push' or 'Ajax push', a technique for pushing messages from the
9
+ server to the client to minimize latency.
10
+
11
+ NOTE this is not currently intended for large-scale use. It is being
12
+ developed mostly as a tool for scripting multiple browsers and automating
13
+ JavaScript testing. However, use of a well-known protocol should allow
14
+ it to be repurposed fairly easily.
15
+
16
+
17
+ == Usage
18
+
19
+ Faye can be installed as middleware in front of any Rack application. For
20
+ example, here's a <tt>config.ru</tt> for running it with Sinatra:
21
+
22
+ require 'rubygems'
23
+ require 'faye'
24
+ require 'sinatra'
25
+ require 'path/to/sinatra/app'
26
+
27
+ use Faye::RackAdapter, :mount => '/comet',
28
+ :timeout => 30
29
+
30
+ run Sinatra::Application
31
+
32
+ This starts a Comet endpoint at <tt>http://localhost:9292/comet</tt> with
33
+ the client script at <tt>http://localhost:9292/comet.js</tt>. The +timeout+
34
+ option sets how long (in seconds) a long-polling request will wait before
35
+ timing out; this must be less than the timeout set on your frontend web server
36
+ so that the Comet server can send a response before Apache (for example)
37
+ closes the connection.
38
+
39
+ In your front-end code, set up the client as follows:
40
+
41
+ <script type="text/javascript" src="/comet.js"></script>
42
+
43
+ <script type="text/javascript">
44
+ CometClient = new Faye.Client('/comet');
45
+ CometClient.connect();
46
+ </script>
47
+
48
+ This client object can then be used to publish and subscribe to named
49
+ channels as follows:
50
+
51
+ CometClient.subscribe('/path/to/channel', function(message) {
52
+ // process received message
53
+ });
54
+
55
+ CometClient.publish('/some/other/channel', {foo: 'bar'});
56
+
57
+ You can publish arbitrary JavaScript objects to a channel, and the object will
58
+ be transmitted as the +message+ parameter to any subscribers to that channel.
59
+ Channel names must be formatted as absolute path names as shown. Channels
60
+ beginning with <tt>/meta/</tt> are reserved for use by the messaging protocol
61
+ and may not be subscribed to.
62
+
63
+
64
+ == Examples
65
+
66
+ See demo apps in +examples+. If you've checked this out from the repo rather than
67
+ installing the gem you will need to build the JavaScript client from source, this
68
+ is done as follows:
69
+
70
+ sudo gem install jake
71
+ jake -f
72
+
73
+
74
+ == To-do
75
+
76
+ * Provide support for user-defined <tt>/service/*</tt> channels
77
+ * Support Thin's async response
78
+ * Allow server to scale to multiple nodes
79
+ * Provide a server-side client
80
+ * Console for scripting browsers from the command line
81
+
82
+
83
+ == Installation
84
+
85
+ sudo gem install hoe faye
86
+
87
+
88
+ == License
89
+
90
+ (The MIT License)
91
+
92
+ Copyright (c) 2009 James Coglan
93
+
94
+ Permission is hereby granted, free of charge, to any person obtaining
95
+ a copy of this software and associated documentation files (the
96
+ 'Software'), to deal in the Software without restriction, including
97
+ without limitation the rights to use, copy, modify, merge, publish,
98
+ distribute, sublicense, and/or sell copies of the Software, and to
99
+ permit persons to whom the Software is furnished to do so, subject to
100
+ the following conditions:
101
+
102
+ The above copyright notice and this permission notice shall be
103
+ included in all copies or substantial portions of the Software.
104
+
105
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
106
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
107
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
108
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
109
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
110
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
111
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/faye.rb'
6
+
7
+ Hoe.spec('faye') do |p|
8
+ # p.rubyforge_name = 'fayex' # if different than lowercase project name
9
+ p.developer('James Coglan', 'jcoglan@googlemail.com')
10
+ p.extra_deps = %w[eventmachine rack json]
11
+ end
12
+
13
+ # vim: syntax=Ruby
data/client/channel.js ADDED
@@ -0,0 +1,135 @@
1
+ Faye.Channel = {
2
+ HANDSHAKE: '<%= Faye::Channel::HANDSHAKE %>',
3
+ CONNECT: '<%= Faye::Channel::CONNECT %>',
4
+ SUBSCRIBE: '<%= Faye::Channel::SUBSCRIBE %>',
5
+ UNSUBSCRIBE: '<%= Faye::Channel::UNSUBSCRIBE %>',
6
+ DISCONNECT: '<%= Faye::Channel::DISCONNECT %>',
7
+
8
+ META: '<%= Faye::Channel::META %>',
9
+ SERVICE: '<%= Faye::Channel::SERVICE %>',
10
+
11
+ isValid: function(name) {
12
+ return Faye.Grammar.CHANNEL_NAME.test(name) ||
13
+ Faye.Grammar.CHANNEL_PATTERN.test(name);
14
+ },
15
+
16
+ parse: function(name) {
17
+ if (!this.isValid(name)) return null;
18
+ return name.split('/').slice(1);
19
+ },
20
+
21
+ isMeta: function(name) {
22
+ var segments = this.parse(name);
23
+ return segments ? (segments[0] === this.META) : null;
24
+ },
25
+
26
+ isService: function(name) {
27
+ var segments = this.parse(name);
28
+ return segments ? (segments[0] === this.SERVICE) : null;
29
+ },
30
+
31
+ isSubscribable: function(name) {
32
+ if (!this.isValid(name)) return null;
33
+ return !this.isMeta(name) && !this.isService(name);
34
+ },
35
+
36
+ Tree: Faye.Class({
37
+ initialize: function(value) {
38
+ this._value = value;
39
+ this._children = {};
40
+ },
41
+
42
+ eachChild: function(block, context) {
43
+ Faye.each(this._children, function(key, subtree) {
44
+ block.call(context, key, subtree);
45
+ });
46
+ },
47
+
48
+ each: function(prefix, block, context) {
49
+ this.eachChild(function(path, subtree) {
50
+ path = prefix.concat(path);
51
+ subtree.each(path, block, context);
52
+ });
53
+ if (this._value !== undefined) block.call(context, prefix, this._value);
54
+ },
55
+
56
+ map: function(block, context) {
57
+ var result = [];
58
+ this.each([], function(path, value) {
59
+ result.push(block.call(context, path, value));
60
+ });
61
+ return result;
62
+ },
63
+
64
+ get: function(name) {
65
+ var tree = this.traverse(name);
66
+ return tree ? tree._value : null;
67
+ },
68
+
69
+ set: function(name, value) {
70
+ var subtree = this.traverse(name, true);
71
+ if (subtree) subtree._value = value;
72
+ },
73
+
74
+ traverse: function(path, createIfAbsent) {
75
+ if (typeof path === 'string') path = Faye.Channel.parse(path);
76
+
77
+ if (path === null) return null;
78
+ if (path.length === 0) return this;
79
+
80
+ var subtree = this._children[path[0]];
81
+ if (!subtree && !createIfAbsent) return null;
82
+ if (!subtree) subtree = this._children[path[0]] = new Faye.Channel.Tree();
83
+
84
+ return subtree.traverse(path.slice(1), createIfAbsent);
85
+ },
86
+
87
+ glob: function(path) {
88
+ if (typeof path === 'string') path = Faye.Channel.parse(path);
89
+
90
+ if (path === null) return [];
91
+ if (path.length === 0) return (this._value === undefined) ? [] : [this._value];
92
+
93
+ var list = [];
94
+
95
+ if (Faye.enumEqual(path, ['*'])) {
96
+ Faye.each(this._children, function(key, subtree) {
97
+ if (subtree._value !== undefined) list.push(subtree._value);
98
+ });
99
+ return list;
100
+ }
101
+
102
+ if (Faye.enumEqual(path, ['**'])) {
103
+ list = this.map(function(key, value) { return value });
104
+ list.pop();
105
+ return list;
106
+ }
107
+
108
+ Faye.each(this._children, function(key, subtree) {
109
+ if (key !== path[0] && key !== '*') return;
110
+ var sublist = subtree.glob(path.slice(1));
111
+ Faye.each(sublist, function(channel) { list.push(channel) });
112
+ });
113
+
114
+ if (this._children['**']) list.push(this._children['**']._value);
115
+ return list;
116
+ }
117
+
118
+ /**
119
+ Tests
120
+
121
+ glob = new Faye.Channel.Tree();
122
+ list = '/foo/bar /foo/boo /foo /foobar /foo/bar/boo /foobar/boo /foo/* /foo/**'.split(' ');
123
+
124
+ Faye.each(list, function(c, i) {
125
+ glob.set(c, i + 1);
126
+ });
127
+
128
+ console.log(glob.glob('/foo/*').sort()); // 1,2,7,8
129
+ console.log(glob.glob('/foo/bar').sort()); // 1,7,8
130
+ console.log(glob.glob('/foo/**').sort()); // 1,2,5,7,8
131
+ console.log(glob.glob('/foo/bar/boo').sort()); // 5,8
132
+ **/
133
+ })
134
+ };
135
+
data/client/client.js ADDED
@@ -0,0 +1,265 @@
1
+ Faye.Client = Faye.Class({
2
+ _UNCONNECTED: {},
3
+ _CONNECTING: {},
4
+ _CONNECTED: {},
5
+ _DISCONNECTED: {},
6
+
7
+ _HANDSHAKE: 'handshake',
8
+ _RETRY: 'retry',
9
+ _NONE: 'none',
10
+
11
+ DEFAULT_ENDPOINT: '<%= Faye::RackAdapter::DEFAULT_ENDPOINT %>',
12
+ MAX_DELAY: <%= Faye::Connection::MAX_DELAY %>,
13
+ INTERVAL: <%= Faye::Connection::INTERVAL * 1000 %>,
14
+
15
+ initialize: function(endpoint) {
16
+ this._endpoint = endpoint || this.DEFAULT_ENDPOINT;
17
+ this._transport = Faye.Transport.get(this);
18
+ this._state = this._UNCONNECTED;
19
+ this._outbox = [];
20
+ this._channels = new Faye.Channel.Tree();
21
+
22
+ this._advice = {reconnect: this._RETRY, interval: this.INTERVAL};
23
+
24
+ Faye.Event.on(Faye.ENV, 'beforeunload', this.disconnect, this);
25
+ },
26
+
27
+ // Request
28
+ // MUST include: * channel
29
+ // * version
30
+ // * supportedConnectionTypes
31
+ // MAY include: * minimumVersion
32
+ // * ext
33
+ // * id
34
+ //
35
+ // Success Response Failed Response
36
+ // MUST include: * channel MUST include: * channel
37
+ // * version * successful
38
+ // * supportedConnectionTypes * error
39
+ // * clientId MAY include: * supportedConnectionTypes
40
+ // * successful * advice
41
+ // MAY include: * minimumVersion * version
42
+ // * advice * minimumVersion
43
+ // * ext * ext
44
+ // * id * id
45
+ // * authSuccessful
46
+ handshake: function(callback, scope) {
47
+ if (this._advice.reconnect === this._NONE) return;
48
+ if (this._state !== this._UNCONNECTED) return;
49
+
50
+ this._state = this._CONNECTING;
51
+ var self = this, id = this.generateId();
52
+
53
+ this._transport.send({
54
+ channel: Faye.Channel.HANDSHAKE,
55
+ version: Faye.BAYEUX_VERSION,
56
+ supportedConnectionTypes: Faye.Transport.supportedConnectionTypes(),
57
+ id: id
58
+
59
+ }, function(response) {
60
+ if (response.id !== id) return;
61
+
62
+ if (!response.successful) {
63
+ setTimeout(function() { self.handshake(callback, scope) }, this._advice.interval);
64
+ return this._state = this._UNCONNECTED;
65
+ }
66
+
67
+ this._state = this._CONNECTED;
68
+ this._clientId = response.clientId;
69
+ this._transport = Faye.Transport.get(this, response.supportedConnectionTypes);
70
+
71
+ if (callback) callback.call(scope);
72
+ }, this);
73
+ },
74
+
75
+ // Request Response
76
+ // MUST include: * channel MUST include: * channel
77
+ // * clientId * successful
78
+ // * connectionType * clientId
79
+ // MAY include: * ext MAY include: * error
80
+ // * id * advice
81
+ // * ext
82
+ // * id
83
+ // * timestamp
84
+ connect: function(callback, scope) {
85
+ if (this._advice.reconnect === this._NONE) return;
86
+
87
+ if (this._advice.reconnect === this._HANDSHAKE || this._state === this._UNCONNECTED)
88
+ return this.handshake(function() { this.connect(callback, scope) }, this);
89
+
90
+ if (this._state !== this._CONNECTED) return;
91
+
92
+ if (this._connectionId) return;
93
+ this._connectionId = this.generateId();
94
+ var self = this;
95
+
96
+ this._transport.send({
97
+ channel: Faye.Channel.CONNECT,
98
+ clientId: this._clientId,
99
+ connectionType: this._transport.connectionType,
100
+ id: this._connectionId
101
+
102
+ }, function(response) {
103
+ if (response.id !== this._connectionId) return;
104
+ delete this._connectionId;
105
+
106
+ if (response.successful)
107
+ this.connect();
108
+ else
109
+ setTimeout(function() { self.connect() }, this._advice.interval);
110
+ }, this);
111
+
112
+ if (callback) callback.call(scope);
113
+ },
114
+
115
+ // Request Response
116
+ // MUST include: * channel MUST include: * channel
117
+ // * clientId * successful
118
+ // MAY include: * ext * clientId
119
+ // * id MAY include: * error
120
+ // * ext
121
+ // * id
122
+ disconnect: function() {
123
+ if (this._state !== this._CONNECTED) return;
124
+ this._state = this._DISCONNECTED;
125
+
126
+ this._transport.send({
127
+ channel: Faye.Channel.DISCONNECT,
128
+ clientId: this._clientId
129
+ });
130
+
131
+ this._channels = new Faye.Channel.Tree();
132
+ },
133
+
134
+ // Request Response
135
+ // MUST include: * channel MUST include: * channel
136
+ // * clientId * successful
137
+ // * subscription * clientId
138
+ // MAY include: * ext * subscription
139
+ // * id MAY include: * error
140
+ // * advice
141
+ // * ext
142
+ // * id
143
+ // * timestamp
144
+ subscribe: function(channels, callback, scope) {
145
+ if (this._state !== this._CONNECTED) return;
146
+
147
+ channels = [].concat(channels);
148
+ this._validateChannels(channels);
149
+
150
+ var id = this.generateId();
151
+
152
+ this._transport.send({
153
+ channel: Faye.Channel.SUBSCRIBE,
154
+ clientId: this._clientId,
155
+ subscription: channels,
156
+ id: id
157
+
158
+ }, function(response) {
159
+ if (response.id !== id) return;
160
+ if (!response.successful) return;
161
+
162
+ channels = [].concat(response.subscription);
163
+ Faye.each(channels, function(channel) {
164
+ this._channels.set(channel, [callback, scope]);
165
+ }, this);
166
+ }, this);
167
+ },
168
+
169
+ // Request Response
170
+ // MUST include: * channel MUST include: * channel
171
+ // * clientId * successful
172
+ // * subscription * clientId
173
+ // MAY include: * ext * subscription
174
+ // * id MAY include: * error
175
+ // * advice
176
+ // * ext
177
+ // * id
178
+ // * timestamp
179
+ unsubscribe: function(channels, callback, scope) {
180
+ if (this._state !== this._CONNECTED) return;
181
+
182
+ channels = [].concat(channels);
183
+ this._validateChannels(channels);
184
+
185
+ var id = this.generateId();
186
+
187
+ this._transport.send({
188
+ channel: Faye.Channel.UNSUBSCRIBE,
189
+ clientId: this._clientId,
190
+ subscription: channels,
191
+ id: id
192
+
193
+ }, function(response) {
194
+ if (response.id !== id) return;
195
+ if (!response.successful) return;
196
+
197
+ channels = [].concat(response.subscription);
198
+ Faye.each(channels, function(channel) {
199
+ this._channels.set(channel, null);
200
+ }, this);
201
+ }, this);
202
+ },
203
+
204
+ // Request Response
205
+ // MUST include: * channel MUST include: * channel
206
+ // * data * successful
207
+ // MAY include: * clientId MAY include: * id
208
+ // * id * error
209
+ // * ext * ext
210
+ publish: function(channel, data) {
211
+ if (this._state !== this._CONNECTED) return;
212
+ this._validateChannels([channel]);
213
+
214
+ this.enqueue({
215
+ channel: channel,
216
+ data: data,
217
+ clientId: this._clientId
218
+ });
219
+
220
+ if (this._timeout) return;
221
+ var self = this;
222
+
223
+ this._timeout = setTimeout(function() {
224
+ delete self._timeout;
225
+ self.flush();
226
+ }, this.MAX_DELAY * 1000);
227
+ },
228
+
229
+ generateId: function(bitdepth) {
230
+ bitdepth = bitdepth || 32;
231
+ return Math.floor(Math.pow(2,bitdepth) * Math.random()).toString(16);
232
+ },
233
+
234
+ enqueue: function(message) {
235
+ this._outbox.push(message);
236
+ },
237
+
238
+ flush: function() {
239
+ this._transport.send(this._outbox);
240
+ this._outbox = [];
241
+ },
242
+
243
+ _validateChannels: function(channels) {
244
+ Faye.each(channels, function(channel) {
245
+ if (!Faye.Channel.isValid(channel))
246
+ throw '"' + channel + '" is not a valid channel name';
247
+ if (!Faye.Channel.isSubscribable(channel))
248
+ throw 'Clients may not subscribe to channel "' + channel + '"';
249
+ });
250
+ },
251
+
252
+ _handleAdvice: function(advice) {
253
+ Faye.extend(this._advice, advice);
254
+ if (this._advice.reconnect === this._HANDSHAKE) this._clientId = null;
255
+ },
256
+
257
+ _sendToSubscribers: function(message) {
258
+ var channels = this._channels.glob(message.channel);
259
+ Faye.each(channels, function(callback) {
260
+ if (!callback) return;
261
+ callback[0].call(callback[1], message.data);
262
+ });
263
+ }
264
+ });
265
+