push 0.0.1
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.
- data/.gitignore +5 -0
- data/.rbenv-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +61 -0
- data/Rakefile +4 -0
- data/lib/push.rb +31 -0
- data/lib/push/backend.rb +63 -0
- data/lib/push/configuration.rb +56 -0
- data/lib/push/logging.rb +13 -0
- data/lib/push/producer.rb +26 -0
- data/lib/push/transport.rb +15 -0
- data/lib/push/transport/controller/http_long_poll.rb +115 -0
- data/lib/push/transport/controller/http_long_poll/public/flash/easyxdm.swf +0 -0
- data/lib/push/transport/controller/http_long_poll/public/javascripts/easyXDM.debug.js +2779 -0
- data/lib/push/transport/controller/http_long_poll/public/javascripts/easyXDM.js +2463 -0
- data/lib/push/transport/controller/http_long_poll/public/javascripts/json2.js +483 -0
- data/lib/push/transport/controller/http_long_poll/views/proxy.erb +205 -0
- data/lib/push/transport/controller/web_socket.rb +46 -0
- data/lib/push/version.rb +3 -0
- data/push.gemspec +27 -0
- data/spec/pusher_spec.rb +147 -0
- data/spec/spec_helper.rb +4 -0
- metadata +127 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Cross-domain XHMLHttpRequest proxy</title>
|
5
|
+
<%= javascript_include_tag *%w[json2 easyXDM] %>
|
6
|
+
<script type="text/javascript">
|
7
|
+
/*
|
8
|
+
* This is a CORS (Cross-Origin Resource Sharing) and AJAX enabled endpoint for easyXDM.
|
9
|
+
* The ACL code is adapted from pmxdr (http://github.com/eligrey/pmxdr/) by Eli Grey (http://eligrey.com/)
|
10
|
+
*
|
11
|
+
*/
|
12
|
+
// From http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
|
13
|
+
function isHostMethod(object, property){
|
14
|
+
var t = typeof object[property];
|
15
|
+
return t == 'function' ||
|
16
|
+
(!!(t == 'object' && object[property])) ||
|
17
|
+
t == 'unknown';
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Creates a cross-browser XMLHttpRequest object
|
22
|
+
* @return {XMLHttpRequest} A XMLHttpRequest object.
|
23
|
+
*/
|
24
|
+
var getXhr = (function(){
|
25
|
+
if (isHostMethod(window, "XMLHttpRequest")) {
|
26
|
+
return function(){
|
27
|
+
return new XMLHttpRequest();
|
28
|
+
};
|
29
|
+
}
|
30
|
+
else {
|
31
|
+
var item = (function(){
|
32
|
+
var list = ["Microsoft", "Msxml2", "Msxml3"], i = list.length;
|
33
|
+
while (i--) {
|
34
|
+
try {
|
35
|
+
item = list[i] + ".XMLHTTP";
|
36
|
+
var obj = new ActiveXObject(item);
|
37
|
+
return item;
|
38
|
+
}
|
39
|
+
catch (e) {
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}());
|
43
|
+
return function(){
|
44
|
+
return new ActiveXObject(item);
|
45
|
+
};
|
46
|
+
}
|
47
|
+
}());
|
48
|
+
|
49
|
+
// this file is by default set up to use Access Control - this means that it will use the headers set by the server to decide whether or not to allow the call to return
|
50
|
+
// TODO turn this back on once the CORS headers are implemented
|
51
|
+
var useAccessControl = false;
|
52
|
+
// always trusted origins, can be exact strings or regular expressions
|
53
|
+
var alwaysTrustedOrigins = [(/\.?easyxdm\.net/), (/xdm1/)];
|
54
|
+
|
55
|
+
// instantiate a new easyXDM object which will handle the request
|
56
|
+
var remote = new easyXDM.Rpc({
|
57
|
+
local: "../name.html",
|
58
|
+
swf: "../easyxdm.swf"
|
59
|
+
}, {
|
60
|
+
local: {
|
61
|
+
// define the exposed method
|
62
|
+
request: function(config, success, error){
|
63
|
+
// apply default values if not set
|
64
|
+
easyXDM.apply(config, {
|
65
|
+
method: "POST",
|
66
|
+
headers: {
|
67
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
68
|
+
"X-Requested-With": "XMLHttpRequest"
|
69
|
+
},
|
70
|
+
success: Function.prototype,
|
71
|
+
error: function(msg){
|
72
|
+
throw new Error(msg);
|
73
|
+
},
|
74
|
+
data: {},
|
75
|
+
timeout: 10 * 1000
|
76
|
+
}, true);
|
77
|
+
|
78
|
+
var isPOST = config.method == "POST";
|
79
|
+
|
80
|
+
// convert the data into a format we can send to the server
|
81
|
+
var pairs = [];
|
82
|
+
for (var key in config.data) {
|
83
|
+
if (config.data.hasOwnProperty(key)) {
|
84
|
+
pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(config.data[key]));
|
85
|
+
}
|
86
|
+
}
|
87
|
+
var data = pairs.join("&");
|
88
|
+
|
89
|
+
// create the XMLHttpRequest object
|
90
|
+
var req = getXhr();
|
91
|
+
req.open(config.method, config.url + (isPOST ? "" : "?" + data), true);
|
92
|
+
|
93
|
+
// apply the request headers
|
94
|
+
for (var prop in config.headers) {
|
95
|
+
if (config.headers.hasOwnProperty(prop) && config.headers[prop]) {
|
96
|
+
req.setRequestHeader(prop, config.headers[prop]);
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
// set a timeout
|
101
|
+
var timeout;
|
102
|
+
timeout = setTimeout(function(){
|
103
|
+
req.abort();
|
104
|
+
|
105
|
+
if(req && req.readyState != 4){
|
106
|
+
error({
|
107
|
+
message: "timeout after " + config.timeout + " second",
|
108
|
+
status: 0,
|
109
|
+
data: null,
|
110
|
+
toString: function(){
|
111
|
+
return this.message + " Status: " + this.status;
|
112
|
+
}
|
113
|
+
}, null);
|
114
|
+
}
|
115
|
+
}, config.timeout);
|
116
|
+
|
117
|
+
// check if this origin should always be trusted
|
118
|
+
var alwaysTrusted = false, i = alwaysTrustedOrigins.length;
|
119
|
+
while (i-- && !alwaysTrusted) {
|
120
|
+
if (alwaysTrustedOrigins[i] instanceof RegExp) {
|
121
|
+
alwaysTrusted = alwaysTrustedOrigins[i].test(remote.origin);
|
122
|
+
}
|
123
|
+
else if (typeof alwaysTrustedOrigins[i] == "string") {
|
124
|
+
alwaysTrusted = (remote.origin === alwaysTrustedOrigins[i]);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
|
129
|
+
// define the onreadystate handler
|
130
|
+
req.onreadystatechange = function(){
|
131
|
+
if (req.readyState == 4) {
|
132
|
+
clearTimeout(timeout);
|
133
|
+
|
134
|
+
// parse the response headers
|
135
|
+
var rawHeaders = req.getAllResponseHeaders(), headers = {}, headers_lowercase = {}, reHeader = /([\w-_]+):\s+(.*)$/gm, m;
|
136
|
+
while ((m = reHeader.exec(rawHeaders))) {
|
137
|
+
headers_lowercase[m[1].toLowerCase()] = headers[m[1]] = m[2];
|
138
|
+
}
|
139
|
+
|
140
|
+
// In IE status 204 is translated to 1223
|
141
|
+
var normalizedStatus = req.status;
|
142
|
+
|
143
|
+
if(normalizedStatus === 1223) {
|
144
|
+
normalizedStatus = 204;
|
145
|
+
}
|
146
|
+
|
147
|
+
if (normalizedStatus < 200 || normalizedStatus >= 300) {
|
148
|
+
if (useAccessControl) {
|
149
|
+
error("INVALID_STATUS_CODE");
|
150
|
+
}
|
151
|
+
else {
|
152
|
+
error("INVALID_STATUS_CODE", {
|
153
|
+
status: normalizedStatus,
|
154
|
+
data: req.responseText
|
155
|
+
});
|
156
|
+
}
|
157
|
+
}
|
158
|
+
else {
|
159
|
+
|
160
|
+
var errorMessage;
|
161
|
+
if (useAccessControl) {
|
162
|
+
// normalize the valuse access controls
|
163
|
+
var aclAllowedOrigin = (headers_lowercase["access-control-allow-origin"] || "").replace(/\s/g, "");
|
164
|
+
var aclAllowedMethods = (headers_lowercase["access-control-allow-methods"] || "").replace(/\s/g, "");
|
165
|
+
|
166
|
+
// determine if origin is trusted
|
167
|
+
if (alwaysTrusted || aclAllowedOrigin == "*" || aclAllowedOrigin.indexOf(remote.origin) != -1) {
|
168
|
+
// determine if the request method was allowed
|
169
|
+
if (aclAllowedMethods && aclAllowedMethods != "*" && aclAllowedMethods.indexOf(config.method) == -1) {
|
170
|
+
errorMessage = "DISALLOWED_REQUEST_METHOD";
|
171
|
+
}
|
172
|
+
}
|
173
|
+
else {
|
174
|
+
errorMessage = "DISALLOWED_ORIGIN";
|
175
|
+
}
|
176
|
+
|
177
|
+
}
|
178
|
+
|
179
|
+
if (errorMessage) {
|
180
|
+
error(errorMessage);
|
181
|
+
}
|
182
|
+
else {
|
183
|
+
success({
|
184
|
+
data: req.responseText,
|
185
|
+
status: normalizedStatus,
|
186
|
+
headers: headers
|
187
|
+
});
|
188
|
+
}
|
189
|
+
}
|
190
|
+
// reset the handler
|
191
|
+
req.onreadystatechange = Function.prototype;
|
192
|
+
req = null;
|
193
|
+
}
|
194
|
+
};
|
195
|
+
|
196
|
+
// issue the request
|
197
|
+
req.send(isPOST ? data : "");
|
198
|
+
}
|
199
|
+
}
|
200
|
+
});
|
201
|
+
</script>
|
202
|
+
</head>
|
203
|
+
<body>
|
204
|
+
</body>
|
205
|
+
</html>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Push::Transport::Controller
|
2
|
+
class WebSocket < Cramp::Websocket
|
3
|
+
include Push::Logger
|
4
|
+
|
5
|
+
on_start :bind_queue
|
6
|
+
on_finish :unbind_queue
|
7
|
+
on_data :message_received
|
8
|
+
|
9
|
+
def bind_queue
|
10
|
+
logger.info "Subscribed to '#{exchange}'"
|
11
|
+
queue.bind(channel.fanout(exchange)).subscribe(:ack => true) {|header, message|
|
12
|
+
header.ack
|
13
|
+
render message
|
14
|
+
logger.info "Sent message: #{message}"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def unbind_queue
|
19
|
+
queue.unsubscribe {
|
20
|
+
channel.close
|
21
|
+
logger.info "Unsubscribed from '#{exchange}'"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def message_received(data)
|
26
|
+
logger.info "Received #{data}" # Who cares? Do nothing.
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def channel
|
31
|
+
@channel ||= AMQP::Channel.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def queue
|
35
|
+
@queue ||= channel.queue("#{sid}@#{exchange}", :arguments => {'x-expires' => Push.config.amqp.queue_ttl})
|
36
|
+
end
|
37
|
+
|
38
|
+
def sid
|
39
|
+
@sid ||= UUID.new.generate
|
40
|
+
end
|
41
|
+
|
42
|
+
def exchange
|
43
|
+
request.env['PATH_INFO']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/push/version.rb
ADDED
data/push.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "push/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "push"
|
7
|
+
s.version = Push::VERSION
|
8
|
+
s.authors = ["Brad Gessler"]
|
9
|
+
s.email = ["brad@bradgessler.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Build realtime Ruby web applications}
|
12
|
+
s.description = %q{Push is a realtime web application toolkit for building realtime Ruby web applications.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "push"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "guard-rspec"
|
24
|
+
s.add_runtime_dependency "rack", ">= 1.1.0"
|
25
|
+
s.add_runtime_dependency "uuid"
|
26
|
+
s.add_runtime_dependency "async_sinatra"
|
27
|
+
end
|
data/spec/pusher_spec.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Push do
|
4
|
+
before(:all) do
|
5
|
+
Push.config.backend = :test
|
6
|
+
end
|
7
|
+
|
8
|
+
context "config" do
|
9
|
+
before(:each) do
|
10
|
+
@env = Push::Configuration.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should have backend" do
|
14
|
+
@env.backend.should eql(:amqp)
|
15
|
+
end
|
16
|
+
|
17
|
+
context "amqp" do
|
18
|
+
it "should default host to 127.0.0.1" do
|
19
|
+
@env.amqp.host.should eql('127.0.0.1')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should default username to guest" do
|
23
|
+
@env.amqp.username.should eql('guest')
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should default password to guest" do
|
27
|
+
@env.amqp.password.should eql('guest')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should default vhost to /" do
|
31
|
+
@env.amqp.vhost.should eql('/')
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have queue_ttl" do
|
35
|
+
@env.amqp.queue_ttl.should eql(5)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should have logger" do
|
40
|
+
@env.logger.should be_instance_of(Logger)
|
41
|
+
end
|
42
|
+
|
43
|
+
context "web_socket_url" do
|
44
|
+
it "should have web_socket_url" do
|
45
|
+
@env.web_socket.url.should eql('ws://localhost:3000/_push')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "long_poll_url" do
|
50
|
+
it "should have long_poll_url" do
|
51
|
+
@env.long_poll.url.should eql('http://localhost:3000/_push')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have long_poll_timeout" do
|
55
|
+
@env.long_poll.timeout.should eql(30)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "from_hash" do
|
60
|
+
before(:each) do
|
61
|
+
@env.from_hash({
|
62
|
+
'backend' => 'test',
|
63
|
+
'web_socket' => {
|
64
|
+
'url' => 'ws://push.polleverywhere.com'
|
65
|
+
},
|
66
|
+
'long_poll' => {
|
67
|
+
'url' => 'http://push.polleverywhere.com'
|
68
|
+
},
|
69
|
+
'amqp' => {
|
70
|
+
'host' => 'intra.push.polleverywhere.net',
|
71
|
+
'port' => 999,
|
72
|
+
'username' => 'brad',
|
73
|
+
'password' => 'fun',
|
74
|
+
'queue_ttl' => 10,
|
75
|
+
'vhost' => 'hi'
|
76
|
+
}
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should config backend" do
|
81
|
+
@env.backend.should eql(:test)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should config web_socket url" do
|
85
|
+
@env.web_socket.url.should eql('ws://push.polleverywhere.com')
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should config long_poll url" do
|
89
|
+
@env.long_poll.url.should eql('http://push.polleverywhere.com')
|
90
|
+
end
|
91
|
+
|
92
|
+
context "amqp" do
|
93
|
+
it "should config host" do
|
94
|
+
@env.amqp.host.should eql('intra.push.polleverywhere.net')
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should config username" do
|
98
|
+
@env.amqp.username.should eql('brad')
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should config password" do
|
102
|
+
@env.amqp.password.should eql('fun')
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should config vhost" do
|
106
|
+
@env.amqp.vhost.should eql('hi')
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should config queue_ttl" do
|
110
|
+
@env.amqp.queue_ttl.should eql(10)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "backend" do
|
118
|
+
context "adapters" do
|
119
|
+
before(:all) do
|
120
|
+
@an_adapter = Class.new
|
121
|
+
end
|
122
|
+
|
123
|
+
it "register new adapter" do
|
124
|
+
lambda{
|
125
|
+
Push::Backend.register_adapter(:super_cool, @an_adapter)
|
126
|
+
}.should change(Push::Backend.adapters, :count).by(1)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should return instance of an adapter" do
|
130
|
+
Push::Backend.register_adapter(:super_cool, @an_adapter)
|
131
|
+
Push::Backend.adapter(:super_cool).should be_an_instance_of(@an_adapter)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "producer" do
|
137
|
+
before(:all) do
|
138
|
+
@producer = Push::Producer.new
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should publish" do
|
142
|
+
lambda{
|
143
|
+
@producer.publish('hi').to('/exchange')
|
144
|
+
}.should change(@producer.backend.exchange['/exchange'], :count).by(1)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: push
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brad Gessler
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-18 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70258581885400 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70258581885400
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: guard-rspec
|
27
|
+
requirement: &70258581884700 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70258581884700
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rack
|
38
|
+
requirement: &70258581883280 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.1.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70258581883280
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: uuid
|
49
|
+
requirement: &70258581873460 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70258581873460
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: async_sinatra
|
60
|
+
requirement: &70258581871780 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70258581871780
|
69
|
+
description: Push is a realtime web application toolkit for building realtime Ruby
|
70
|
+
web applications.
|
71
|
+
email:
|
72
|
+
- brad@bradgessler.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- .rbenv-version
|
79
|
+
- Gemfile
|
80
|
+
- Guardfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/push.rb
|
84
|
+
- lib/push/backend.rb
|
85
|
+
- lib/push/configuration.rb
|
86
|
+
- lib/push/logging.rb
|
87
|
+
- lib/push/producer.rb
|
88
|
+
- lib/push/transport.rb
|
89
|
+
- lib/push/transport/controller/http_long_poll.rb
|
90
|
+
- lib/push/transport/controller/http_long_poll/public/flash/easyxdm.swf
|
91
|
+
- lib/push/transport/controller/http_long_poll/public/javascripts/easyXDM.debug.js
|
92
|
+
- lib/push/transport/controller/http_long_poll/public/javascripts/easyXDM.js
|
93
|
+
- lib/push/transport/controller/http_long_poll/public/javascripts/json2.js
|
94
|
+
- lib/push/transport/controller/http_long_poll/views/proxy.erb
|
95
|
+
- lib/push/transport/controller/web_socket.rb
|
96
|
+
- lib/push/version.rb
|
97
|
+
- push.gemspec
|
98
|
+
- spec/pusher_spec.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
homepage: ''
|
101
|
+
licenses: []
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project: push
|
120
|
+
rubygems_version: 1.8.10
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Build realtime Ruby web applications
|
124
|
+
test_files:
|
125
|
+
- spec/pusher_spec.rb
|
126
|
+
- spec/spec_helper.rb
|
127
|
+
has_rdoc:
|