diff options
-rw-r--r-- | examples/clock/src/toppage_handler.erl | 48 | ||||
-rw-r--r-- | priv/bullet.js | 54 | ||||
-rw-r--r-- | src/bullet_handler.erl | 68 |
3 files changed, 142 insertions, 28 deletions
diff --git a/examples/clock/src/toppage_handler.erl b/examples/clock/src/toppage_handler.erl index ee482a6..b9c6504 100644 --- a/examples/clock/src/toppage_handler.erl +++ b/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> diff --git a/priv/bullet.js b/priv/bullet.js index fe4df41..50686f6 100644 --- a/priv/bullet.js +++ b/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 diff --git a/src/bullet_handler.erl b/src/bullet_handler.erl index 6c42c86..55a81d9 100644 --- a/src/bullet_handler.erl +++ b/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}. |