Browse files

Merge branch 'eventsource' of git://github.com/jdavisp3/bullet

  • Loading branch information...
2 parents d019120 + 954b4f2 commit 14b83979aa35d0ca2241819b26c2d22865dbdcd5 @essen essen committed Jun 7, 2013
Showing with 142 additions and 28 deletions.
  1. +32 −16 examples/clock/src/toppage_handler.erl
  2. +53 −1 priv/bullet.js
  3. +57 −11 src/bullet_handler.erl
View
48 examples/clock/src/toppage_handler.erl
@@ -20,8 +20,14 @@ handle(Req, State) ->
</head>
<body>
- <p>Connection status: <span id=\"status\">bullet not started</span></p>
- <p>Current time: <span id=\"time\">unknown</span></p>
+ <p>Current time (best source): <span id=\"time_best\">unknown</span>
+ <span> </span><span id=\"status_best\">unknown</span></p>
+ <p>Current time (websocket only): <span id=\"time_websocket\">unknown</span>
+ <span> </span><span id=\"status_websocket\">unknown</span></p>
+ <p>Current time (eventsource only): <span id=\"time_eventsource\">unknown</span>
+ <span> </span><span id=\"status_eventsource\">unknown</span></p>
+ <p>Current time (polling only): <span id=\"time_polling\">unknown</span>
+ <span> </span><span id=\"status_polling\">unknown</span></p>
<script
src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js\">
@@ -30,22 +36,32 @@ handle(Req, State) ->
<script type=\"text/javascript\">
// <![CDATA[
$(document).ready(function(){
- var bullet = $.bullet('ws://localhost:8080/bullet');
- bullet.onopen = function(){
- $('#status').text('online');
- };
- bullet.ondisconnect = function(){
- $('#status').text('offline');
- };
- bullet.onmessage = function(e){
- if (e.data != 'pong'){
- $('#time').text(e.data);
+ var start = function(name, options) {
+ var bullet = $.bullet('ws://localhost:8080/bullet', options);
+ bullet.onopen = function(){
+ $('#status_' + name).text('online');
+ };
+ bullet.ondisconnect = function(){
+ $('#status_' + name).text('offline');
+ };
+ bullet.onmessage = function(e){
+ if (e.data != 'pong'){
+ $('#time_' + name).text(e.data);
+ }
+ };
+ bullet.onheartbeat = function(){
+ console.log('ping: ' + name);
+ bullet.send('ping');
}
};
- bullet.onheartbeat = function(){
- console.log('ping');
- bullet.send('ping');
- }
+
+ start('best', {});
+ start('websocket', {'disableEventSource': true,
+ 'disableXHRPolling': true});
+ start('eventsource', {'disableWebSocket': true,
+ 'disableXHRPolling': true});
+ start('polling', {'disableWebSocket': true,
+ 'disableEventSource': true});
});
// ]]>
</script>
View
54 priv/bullet.js
@@ -32,7 +32,7 @@
onheartbeat is called once every few seconds to allow you to easily setup
a ping/pong mechanism.
*/
-(function($){$.extend({bullet: function(url){
+(function($){$.extend({bullet: function(url, options){
var CONNECTING = 0;
var OPEN = 1;
var CLOSING = 2;
@@ -47,6 +47,10 @@
websocket: function(){
var transport = null;
+ if (options !== undefined && options.disableWebSocket) {
+ return false;
+ }
+
if (window.WebSocket){
transport = window.WebSocket;
}
@@ -63,7 +67,54 @@
return null;
},
+ eventsource: function(){
+ if (options !== undefined && options.disableEventSource) {
+ return false;
+ }
+
+ if (!window.EventSource){
+ return false;
+ }
+
+ var eventsourceURL = url.replace('ws:', 'http:').replace('wss:', 'https:');
+ var source = new window.EventSource(eventsourceURL);
+
+ source.onopen = function () {
+ fake.readyState = OPEN;
+ fake.onopen();
+ };
+
+ source.onmessage = function (event) {
+ fake.onmessage(event);
+ };
+
+ source.onerror = function () {
+ source.close(); // bullet will handle reconnects
+ source = undefined;
+ fake.onerror();
+ };
+
+ var fake = {
+ readyState: CONNECTING,
+ send: function(data){
+ return false; // fallback to another method instead?
+ },
+ close: function(){
+ fake.readyState = CLOSED;
+ source.close();
+ source = undefined;
+ fake.onclose();
+ }
+ };
+
+ return {'heart': false, 'transport': function(){ return fake; }};
+ },
+
xhrPolling: function(){
+ if (options !== undefined && options.disableXHRPolling) {
+ return false;
+ }
+
var timeout;
var xhr;
@@ -216,6 +267,7 @@
if (readyState == CLOSING){
readyState = CLOSED;
+ transport = false;
stream.onclose();
} else{
// Close happened on connect, select next transport
View
68 src/bullet_handler.erl
@@ -27,7 +27,9 @@
-record(state, {
handler :: module(),
- handler_state :: term()
+ handler_state :: term(),
+ % poll or eventsource for GET requests
+ get_mode :: 'undefined' | 'poll' | 'eventsource'
}).
-define(TIMEOUT, 60000). %% @todo Configurable.
@@ -52,13 +54,19 @@ init(Transport, Req, Opts) ->
init(Transport, Req, Opts, <<"GET">>) ->
{handler, Handler} = lists:keyfind(handler, 1, Opts),
State = #state{handler=Handler},
- case Handler:init(Transport, Req, Opts, once) of
- {ok, Req2, HandlerState} ->
- Req3 = cowboy_req:compact(Req2),
- {loop, Req3, State#state{handler_state=HandlerState},
- ?TIMEOUT, hibernate};
- {shutdown, Req2, HandlerState} ->
- {shutdown, Req2, State#state{handler_state=HandlerState}}
+ {GetMode, Req2} = get_mode(Req),
+ Active = case GetMode of
+ poll -> once;
+ eventsource -> true
+ end,
+ case Handler:init(Transport, Req2, Opts, Active) of
+ {ok, Req3, HandlerState} ->
+ {ok, Req4} = start_get_mode(GetMode, Req3),
+ Req5 = cowboy_req:compact(Req4),
+ {loop, Req5, State#state{handler_state=HandlerState,
+ get_mode=GetMode}, ?TIMEOUT, hibernate};
+ {shutdown, Req3, HandlerState} ->
+ {shutdown, Req3, State#state{handler_state=HandlerState}}
end;
init(Transport, Req, Opts, <<"POST">>) ->
{handler, Handler} = lists:keyfind(handler, 1, Opts),
@@ -94,13 +102,19 @@ handle(Req, State=#state{handler=Handler, handler_state=HandlerState},
end.
info(Message, Req,
- State=#state{handler=Handler, handler_state=HandlerState}) ->
+ State=#state{get_mode=GetMode, handler=Handler,
+ handler_state=HandlerState}) ->
case Handler:info(Message, Req, HandlerState) of
{ok, Req2, HandlerState2} ->
{loop, Req2, State#state{handler_state=HandlerState2}, hibernate};
{reply, Data, Req2, HandlerState2} ->
- {ok, Req3} = cowboy_req:reply(200, [], Data, Req2),
- {ok, Req3, State#state{handler_state=HandlerState2}}
+ State2 = State#state{handler_state=HandlerState2},
+ case reply_get_mode(GetMode, Data, Req2) of
+ {ok, Req3} ->
+ {ok, Req3, State2};
+ {loop, Req3} ->
+ {loop, Req3, State2, hibernate}
+ end
end.
terminate(_Reason, _Req, undefined) ->
@@ -147,3 +161,35 @@ websocket_info(Info, Req, State=#state{
websocket_terminate(_Reason, Req,
#state{handler=Handler, handler_state=HandlerState}) ->
Handler:terminate(Req, HandlerState).
+
+%% Eventsource and poll utilities
+
+get_mode(Req) ->
+ case cowboy_req:parse_header(<<"accept">>, Req) of
+ {ok, Accepts, Req2} ->
+ get_mode(Accepts, Req2);
+ _ ->
+ {poll, Req}
+ end.
+
+get_mode([{{<<"text">>, <<"event-stream">>, _}, _, _}|_], Req) ->
+ {eventsource, Req};
+get_mode([_|Accepts], Req) ->
+ get_mode(Accepts, Req);
+get_mode([], Req) ->
+ {poll, Req}.
+
+start_get_mode(poll, Req) ->
+ {ok, Req};
+start_get_mode(eventsource, Req) ->
+ Headers = [{<<"content-type">>, <<"text/event-stream">>}],
+ {ok, _} = cowboy_req:chunked_reply(200, Headers, Req).
+
+reply_get_mode(poll, Data, Req) ->
+ {ok, _} = cowboy_req:reply(200, [], Data, Req);
+reply_get_mode(eventsource, Data, Req) ->
+ Bin = iolist_to_binary(Data),
+ Event = [[<<"data: ">>, Line, <<"\n">>] ||
+ Line <- binary:split(Bin, [<<"\r\n">>, <<"\r">>, <<"\n">>], [global])],
+ ok = cowboy_req:chunk([Event, <<"\n">>], Req),
+ {loop, Req}.

0 comments on commit 14b8397

Please sign in to comment.