noah 0.0.5-jruby → 0.1-jruby
Sign up to get free protection for your applications and to get access to all the features.
- 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
|