noah 0.0.5-jruby → 0.1-jruby
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/.gemtest +0 -0
- data/.gitignore +10 -0
- data/LICENSE +201 -0
- data/README.md +68 -212
- data/Rakefile +65 -41
- data/TODO.md +65 -0
- data/bin/noah +2 -1
- data/bin/noah-watcher.rb +103 -0
- data/config.ru +6 -3
- data/config/warble.rb +18 -0
- data/examples/README.md +116 -0
- data/examples/cluster.ru +2 -0
- data/examples/custom-watcher.rb +10 -0
- data/examples/httpclient-server.rb +7 -0
- data/examples/httpclient.rb +12 -0
- data/examples/httpclient2.rb +28 -0
- data/examples/js/FABridge.js +1452 -0
- data/examples/js/WebSocketMain.swf +830 -0
- data/examples/js/swfobject.js +851 -0
- data/examples/js/web_socket.js +312 -0
- data/examples/logger.rb +11 -0
- data/examples/reconfiguring-sinatra-watcher.rb +11 -0
- data/examples/reconfiguring-sinatra.rb +33 -0
- data/examples/simple-post.rb +17 -0
- data/examples/websocket.html +24 -0
- data/examples/websocket.rb +41 -0
- data/lib/noah.rb +6 -8
- data/lib/noah/app.rb +20 -268
- data/lib/noah/application_routes.rb +70 -0
- data/lib/noah/ark.rb +0 -0
- data/lib/noah/configuration_routes.rb +81 -0
- data/lib/noah/custom_watcher.rb +79 -0
- data/lib/noah/ephemeral_routes.rb +47 -0
- data/lib/noah/helpers.rb +37 -14
- data/lib/noah/host_routes.rb +69 -0
- data/lib/noah/models.rb +86 -5
- data/lib/noah/models/applications.rb +41 -0
- data/lib/noah/models/configurations.rb +49 -0
- data/lib/noah/models/ephemerals.rb +54 -0
- data/lib/noah/models/hosts.rb +56 -0
- data/lib/noah/models/services.rb +54 -0
- data/lib/noah/models/watchers.rb +62 -0
- data/lib/noah/passthrough.rb +11 -0
- data/lib/noah/service_routes.rb +71 -0
- data/lib/noah/validations.rb +1 -0
- data/lib/noah/validations/watcher_validations.rb +48 -0
- data/lib/noah/version.rb +1 -1
- data/lib/noah/watcher_routes.rb +45 -0
- data/noah.gemspec +25 -17
- data/spec/application_spec.rb +30 -30
- data/spec/configuration_spec.rb +78 -14
- data/spec/ephemeral_spec.rb +59 -0
- data/spec/host_spec.rb +21 -21
- data/spec/noahapp_application_spec.rb +6 -6
- data/spec/noahapp_configuration_spec.rb +5 -5
- data/spec/noahapp_ephemeral_spec.rb +115 -0
- data/spec/noahapp_host_spec.rb +3 -3
- data/spec/noahapp_service_spec.rb +10 -10
- data/spec/noahapp_watcher_spec.rb +123 -0
- data/spec/service_spec.rb +27 -27
- data/spec/spec_helper.rb +13 -22
- data/spec/support/db/.keep +0 -0
- data/spec/support/test-redis.conf +8 -0
- data/spec/watcher_spec.rb +62 -0
- data/views/index.haml +21 -15
- metadata +189 -146
- data/Gemfile.lock +0 -83
- data/doc/coverage/index.html +0 -138
- data/doc/coverage/jquery-1.3.2.min.js +0 -19
- data/doc/coverage/jquery.tablesorter.min.js +0 -15
- data/doc/coverage/lib-helpers_rb.html +0 -393
- data/doc/coverage/lib-models_rb.html +0 -1449
- data/doc/coverage/noah_rb.html +0 -2019
- data/doc/coverage/print.css +0 -12
- data/doc/coverage/rcov.js +0 -42
- data/doc/coverage/screen.css +0 -270
- data/lib/noah/applications.rb +0 -46
- data/lib/noah/configurations.rb +0 -49
- data/lib/noah/hosts.rb +0 -54
- data/lib/noah/services.rb +0 -57
- data/lib/noah/watchers.rb +0 -18
@@ -0,0 +1,312 @@
|
|
1
|
+
// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
|
2
|
+
// Lincense: New BSD Lincense
|
3
|
+
// Reference: http://dev.w3.org/html5/websockets/
|
4
|
+
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
|
5
|
+
|
6
|
+
(function() {
|
7
|
+
|
8
|
+
if (window.WebSocket) return;
|
9
|
+
|
10
|
+
var console = window.console;
|
11
|
+
if (!console) console = {log: function(){ }, error: function(){ }};
|
12
|
+
|
13
|
+
function hasFlash() {
|
14
|
+
if ('navigator' in window && 'plugins' in navigator && navigator.plugins['Shockwave Flash']) {
|
15
|
+
return !!navigator.plugins['Shockwave Flash'].description;
|
16
|
+
}
|
17
|
+
if ('ActiveXObject' in window) {
|
18
|
+
try {
|
19
|
+
return !!new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
|
20
|
+
} catch (e) {}
|
21
|
+
}
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
|
25
|
+
if (!hasFlash()) {
|
26
|
+
console.error("Flash Player is not installed.");
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
WebSocket = function(url, protocol, proxyHost, proxyPort, headers) {
|
31
|
+
var self = this;
|
32
|
+
self.readyState = WebSocket.CONNECTING;
|
33
|
+
self.bufferedAmount = 0;
|
34
|
+
WebSocket.__addTask(function() {
|
35
|
+
self.__flash =
|
36
|
+
WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null);
|
37
|
+
|
38
|
+
self.__flash.addEventListener("open", function(fe) {
|
39
|
+
try {
|
40
|
+
if (self.onopen) self.onopen();
|
41
|
+
} catch (e) {
|
42
|
+
console.error(e.toString());
|
43
|
+
}
|
44
|
+
});
|
45
|
+
|
46
|
+
self.__flash.addEventListener("close", function(fe) {
|
47
|
+
try {
|
48
|
+
if (self.onclose) self.onclose();
|
49
|
+
} catch (e) {
|
50
|
+
console.error(e.toString());
|
51
|
+
}
|
52
|
+
});
|
53
|
+
|
54
|
+
self.__flash.addEventListener("message", function(fe) {
|
55
|
+
var data = decodeURIComponent(fe.getData());
|
56
|
+
try {
|
57
|
+
if (self.onmessage) {
|
58
|
+
var e;
|
59
|
+
if (window.MessageEvent) {
|
60
|
+
e = document.createEvent("MessageEvent");
|
61
|
+
e.initMessageEvent("message", false, false, data, null, null, window);
|
62
|
+
} else { // IE
|
63
|
+
e = {data: data};
|
64
|
+
}
|
65
|
+
self.onmessage(e);
|
66
|
+
}
|
67
|
+
} catch (e) {
|
68
|
+
console.error(e.toString());
|
69
|
+
}
|
70
|
+
});
|
71
|
+
|
72
|
+
self.__flash.addEventListener("stateChange", function(fe) {
|
73
|
+
try {
|
74
|
+
self.readyState = fe.getReadyState();
|
75
|
+
self.bufferedAmount = fe.getBufferedAmount();
|
76
|
+
} catch (e) {
|
77
|
+
console.error(e.toString());
|
78
|
+
}
|
79
|
+
});
|
80
|
+
|
81
|
+
//console.log("[WebSocket] Flash object is ready");
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
WebSocket.prototype.send = function(data) {
|
86
|
+
if (!this.__flash || this.readyState == WebSocket.CONNECTING) {
|
87
|
+
throw "INVALID_STATE_ERR: Web Socket connection has not been established";
|
88
|
+
}
|
89
|
+
var result = this.__flash.send(data);
|
90
|
+
if (result < 0) { // success
|
91
|
+
return true;
|
92
|
+
} else {
|
93
|
+
this.bufferedAmount = result;
|
94
|
+
return false;
|
95
|
+
}
|
96
|
+
};
|
97
|
+
|
98
|
+
WebSocket.prototype.close = function() {
|
99
|
+
if (!this.__flash) return;
|
100
|
+
if (this.readyState != WebSocket.OPEN) return;
|
101
|
+
this.__flash.close();
|
102
|
+
// Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events
|
103
|
+
// which causes weird error:
|
104
|
+
// > You are trying to call recursively into the Flash Player which is not allowed.
|
105
|
+
this.readyState = WebSocket.CLOSED;
|
106
|
+
if (this.onclose) this.onclose();
|
107
|
+
};
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
|
111
|
+
*
|
112
|
+
* @param {string} type
|
113
|
+
* @param {function} listener
|
114
|
+
* @param {boolean} useCapture !NB Not implemented yet
|
115
|
+
* @return void
|
116
|
+
*/
|
117
|
+
WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
|
118
|
+
if (!('__events' in this)) {
|
119
|
+
this.__events = {};
|
120
|
+
}
|
121
|
+
if (!(type in this.__events)) {
|
122
|
+
this.__events[type] = [];
|
123
|
+
if ('function' == typeof this['on' + type]) {
|
124
|
+
this.__events[type].defaultHandler = this['on' + type];
|
125
|
+
this['on' + type] = WebSocket_FireEvent(this, type);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
this.__events[type].push(listener);
|
129
|
+
};
|
130
|
+
|
131
|
+
/**
|
132
|
+
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
|
133
|
+
*
|
134
|
+
* @param {string} type
|
135
|
+
* @param {function} listener
|
136
|
+
* @param {boolean} useCapture NB! Not implemented yet
|
137
|
+
* @return void
|
138
|
+
*/
|
139
|
+
WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
|
140
|
+
if (!('__events' in this)) {
|
141
|
+
this.__events = {};
|
142
|
+
}
|
143
|
+
if (!(type in this.__events)) return;
|
144
|
+
for (var i = this.__events.length; i > -1; --i) {
|
145
|
+
if (listener === this.__events[type][i]) {
|
146
|
+
this.__events[type].splice(i, 1);
|
147
|
+
break;
|
148
|
+
}
|
149
|
+
}
|
150
|
+
};
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
|
154
|
+
*
|
155
|
+
* @param {WebSocketEvent} event
|
156
|
+
* @return void
|
157
|
+
*/
|
158
|
+
WebSocket.prototype.dispatchEvent = function(event) {
|
159
|
+
if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
|
160
|
+
if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
|
161
|
+
|
162
|
+
for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) {
|
163
|
+
this.__events[event.type][i](event);
|
164
|
+
if (event.cancelBubble) break;
|
165
|
+
}
|
166
|
+
|
167
|
+
if (false !== event.returnValue &&
|
168
|
+
'function' == typeof this.__events[event.type].defaultHandler)
|
169
|
+
{
|
170
|
+
this.__events[event.type].defaultHandler(event);
|
171
|
+
}
|
172
|
+
};
|
173
|
+
|
174
|
+
/**
|
175
|
+
*
|
176
|
+
* @param {object} object
|
177
|
+
* @param {string} type
|
178
|
+
*/
|
179
|
+
function WebSocket_FireEvent(object, type) {
|
180
|
+
return function(data) {
|
181
|
+
var event = new WebSocketEvent();
|
182
|
+
event.initEvent(type, true, true);
|
183
|
+
event.target = event.currentTarget = object;
|
184
|
+
for (var key in data) {
|
185
|
+
event[key] = data[key];
|
186
|
+
}
|
187
|
+
object.dispatchEvent(event, arguments);
|
188
|
+
};
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Basic implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface">DOM 2 EventInterface</a>}
|
193
|
+
*
|
194
|
+
* @class
|
195
|
+
* @constructor
|
196
|
+
*/
|
197
|
+
function WebSocketEvent(){}
|
198
|
+
|
199
|
+
/**
|
200
|
+
*
|
201
|
+
* @type boolean
|
202
|
+
*/
|
203
|
+
WebSocketEvent.prototype.cancelable = true;
|
204
|
+
|
205
|
+
/**
|
206
|
+
*
|
207
|
+
* @type boolean
|
208
|
+
*/
|
209
|
+
WebSocketEvent.prototype.cancelBubble = false;
|
210
|
+
|
211
|
+
/**
|
212
|
+
*
|
213
|
+
* @return void
|
214
|
+
*/
|
215
|
+
WebSocketEvent.prototype.preventDefault = function() {
|
216
|
+
if (this.cancelable) {
|
217
|
+
this.returnValue = false;
|
218
|
+
}
|
219
|
+
};
|
220
|
+
|
221
|
+
/**
|
222
|
+
*
|
223
|
+
* @return void
|
224
|
+
*/
|
225
|
+
WebSocketEvent.prototype.stopPropagation = function() {
|
226
|
+
this.cancelBubble = true;
|
227
|
+
};
|
228
|
+
|
229
|
+
/**
|
230
|
+
*
|
231
|
+
* @param {string} eventTypeArg
|
232
|
+
* @param {boolean} canBubbleArg
|
233
|
+
* @param {boolean} cancelableArg
|
234
|
+
* @return void
|
235
|
+
*/
|
236
|
+
WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) {
|
237
|
+
this.type = eventTypeArg;
|
238
|
+
this.cancelable = cancelableArg;
|
239
|
+
this.timeStamp = new Date();
|
240
|
+
};
|
241
|
+
|
242
|
+
|
243
|
+
WebSocket.CONNECTING = 0;
|
244
|
+
WebSocket.OPEN = 1;
|
245
|
+
WebSocket.CLOSED = 2;
|
246
|
+
|
247
|
+
WebSocket.__tasks = [];
|
248
|
+
|
249
|
+
WebSocket.__initialize = function() {
|
250
|
+
if (!WebSocket.__swfLocation) {
|
251
|
+
//console.error("[WebSocket] set WebSocket.__swfLocation to location of WebSocketMain.swf");
|
252
|
+
//return;
|
253
|
+
WebSocket.__swfLocation = "js/WebSocketMain.swf";
|
254
|
+
}
|
255
|
+
var container = document.createElement("div");
|
256
|
+
container.id = "webSocketContainer";
|
257
|
+
// Puts the Flash out of the window. Note that we cannot use display: none or visibility: hidden
|
258
|
+
// here because it prevents Flash from loading at least in IE.
|
259
|
+
container.style.position = "absolute";
|
260
|
+
container.style.left = "-100px";
|
261
|
+
container.style.top = "-100px";
|
262
|
+
var holder = document.createElement("div");
|
263
|
+
holder.id = "webSocketFlash";
|
264
|
+
container.appendChild(holder);
|
265
|
+
document.body.appendChild(container);
|
266
|
+
swfobject.embedSWF(
|
267
|
+
WebSocket.__swfLocation, "webSocketFlash", "8", "8", "9.0.0",
|
268
|
+
null, {bridgeName: "webSocket"}, null, null,
|
269
|
+
function(e) {
|
270
|
+
if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed");
|
271
|
+
}
|
272
|
+
);
|
273
|
+
FABridge.addInitializationCallback("webSocket", function() {
|
274
|
+
try {
|
275
|
+
//console.log("[WebSocket] FABridge initializad");
|
276
|
+
WebSocket.__flash = FABridge.webSocket.root();
|
277
|
+
WebSocket.__flash.setCallerUrl(location.href);
|
278
|
+
for (var i = 0; i < WebSocket.__tasks.length; ++i) {
|
279
|
+
WebSocket.__tasks[i]();
|
280
|
+
}
|
281
|
+
WebSocket.__tasks = [];
|
282
|
+
} catch (e) {
|
283
|
+
console.error("[WebSocket] " + e.toString());
|
284
|
+
}
|
285
|
+
});
|
286
|
+
};
|
287
|
+
|
288
|
+
WebSocket.__addTask = function(task) {
|
289
|
+
if (WebSocket.__flash) {
|
290
|
+
task();
|
291
|
+
} else {
|
292
|
+
WebSocket.__tasks.push(task);
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
// called from Flash
|
297
|
+
function webSocketLog(message) {
|
298
|
+
console.log(decodeURIComponent(message));
|
299
|
+
}
|
300
|
+
|
301
|
+
// called from Flash
|
302
|
+
function webSocketError(message) {
|
303
|
+
console.error(decodeURIComponent(message));
|
304
|
+
}
|
305
|
+
|
306
|
+
if (window.addEventListener) {
|
307
|
+
window.addEventListener("load", WebSocket.__initialize, false);
|
308
|
+
} else {
|
309
|
+
window.attachEvent("onload", WebSocket.__initialize);
|
310
|
+
}
|
311
|
+
|
312
|
+
})();
|
data/examples/logger.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), '..','lib','noah','custom_watcher')
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
class LoggingWatcher < Noah::CustomWatcher
|
7
|
+
redis_host "redis://127.0.0.1:6379/0"
|
8
|
+
pattern "//noah"
|
9
|
+
destination Proc.new {|x| log = Logger.new(STDOUT); log.debug(x)}
|
10
|
+
run!
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), '..','lib','noah','custom_watcher')
|
4
|
+
require 'em-http-request'
|
5
|
+
|
6
|
+
class HttpPostWatch < Noah::CustomWatcher
|
7
|
+
redis_host "redis://127.0.0.1:6379/0"
|
8
|
+
pattern "//noah/configuration/redis_server"
|
9
|
+
destination Proc.new {|x| ::EM::HttpRequest.new('http://localhost:4567/webhook', :connection_timeout => 2, :inactivity_timeout => 4).post :body => x}
|
10
|
+
run!
|
11
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'sinatra'
|
3
|
+
require 'ohm'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
set :noah_server, 'http://localhost:5678'
|
8
|
+
set :noah_client_name, 'my_sinatra_app'
|
9
|
+
|
10
|
+
def get_config_from_noah(setting)
|
11
|
+
begin
|
12
|
+
c = open("#{settings.noah_server}/c/#{settings.noah_client_name}/#{setting}").read
|
13
|
+
set setting.to_sym, c
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
get_config_from_noah('redis_server')
|
18
|
+
|
19
|
+
def get_redis_version
|
20
|
+
Ohm.connect :url => settings.redis_server
|
21
|
+
Ohm.redis.info["redis_version"]
|
22
|
+
end
|
23
|
+
|
24
|
+
get "/" do
|
25
|
+
"Redis version: #{get_redis_version}"
|
26
|
+
end
|
27
|
+
|
28
|
+
put "/webhook" do
|
29
|
+
data = JSON.parse(request.body.read)
|
30
|
+
settings.redis_server = data["body"]
|
31
|
+
resp = {:message => "reconfigured", :setting => data["name"], :body => data["body"]}.to_json
|
32
|
+
"#{resp}"
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class NoahPostDemo < Sinatra::Base
|
5
|
+
configure do
|
6
|
+
set :app_file, __FILE__
|
7
|
+
set :server, %w[thin]
|
8
|
+
set :logging, true
|
9
|
+
set :run, true
|
10
|
+
end
|
11
|
+
|
12
|
+
post '/webhook/?' do
|
13
|
+
x = request.body.read
|
14
|
+
p JSON.load(x)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
|
4
|
+
<script src='js/swfobject.js'></script>
|
5
|
+
<script src='js/FABridge.js'></script>
|
6
|
+
<script src='js/web_socket.js'></script>
|
7
|
+
<script>
|
8
|
+
$(document).ready(function(){
|
9
|
+
function debug(str){ $("#debug").append("<p>" + str); };
|
10
|
+
|
11
|
+
ws = new WebSocket("ws://localhost:3009/");
|
12
|
+
ws.onmessage = function(evt) { $("#msg").append("<p>"+evt.data+"</p>"); };
|
13
|
+
ws.onclose = function() { debug("socket closed"); };
|
14
|
+
ws.onopen = function() {
|
15
|
+
debug("connected...");
|
16
|
+
};
|
17
|
+
});
|
18
|
+
</script>
|
19
|
+
</head>
|
20
|
+
<body>
|
21
|
+
<div id="debug"></div>
|
22
|
+
<div id="msg"></div>
|
23
|
+
</body>
|
24
|
+
</html>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'em-websocket'
|
5
|
+
require 'em-hiredis'
|
6
|
+
require 'thin'
|
7
|
+
require 'noah'
|
8
|
+
## Uncomment the following to hardcode a redis url
|
9
|
+
#ENV['REDIS_URL'] = "redis://localhost:6379/0"
|
10
|
+
|
11
|
+
EventMachine.run do
|
12
|
+
|
13
|
+
# Passing messages...like a boss
|
14
|
+
@channel = EventMachine::Channel.new
|
15
|
+
|
16
|
+
Thin::Server.start Noah::App
|
17
|
+
r = EventMachine::Hiredis::Client.connect
|
18
|
+
r.psubscribe("//noah/*")
|
19
|
+
r.on(:pmessage) do |pattern, event, message|
|
20
|
+
@channel.push "(#{event}) #{message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 3009) do |ws|
|
24
|
+
ws.onopen {
|
25
|
+
sub = @channel.subscribe { |msg|
|
26
|
+
ws.send msg
|
27
|
+
}
|
28
|
+
|
29
|
+
@channel.push "#{sub} connected and waiting...."
|
30
|
+
|
31
|
+
ws.onmessage { |msg|
|
32
|
+
@channel.push "<#{sub}>: #{msg}"
|
33
|
+
}
|
34
|
+
|
35
|
+
ws.onclose {
|
36
|
+
@channel.unsubscribe(sub)
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|