diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e47894 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +log/* +ebin/*.beam +ebin/*.app +erl_crash.dump +!.gitignore +deps diff --git a/ebin/.gitignore b/ebin/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/priv/static/bullet.html b/priv/static/bullet.html new file mode 100644 index 0000000..49ad828 --- /dev/null +++ b/priv/static/bullet.html @@ -0,0 +1,42 @@ + + Bullet Awesomeness! + + + + + +
+
+ + +
+
+ + diff --git a/priv/static/bullet.js b/priv/static/bullet.js new file mode 100644 index 0000000..e6a4268 --- /dev/null +++ b/priv/static/bullet.js @@ -0,0 +1,247 @@ +/* + Copyright (c) 2011, Loïc Hoguin + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/** + Bullet is a client-side javascript library AND server-side Cowboy handler + to manage continuous streaming. It selects the proper transport in a fully + automated way and makes sure to always reconnect to the server on any + disconnect. You only need to handle sending messages, receiving them, + and managing the heartbeat of the stream. + + Usage: $.bullet(url); + + Then you can register one of the 4 event handlers: + onopen, onmessage, onclose, onheartbeat. + + onopen is called once right after starting the bullet stream. + onmessage is called once for each message receveid. + onclose is called once right after you voluntarily close the socket. + onheartbeat is called once every few seconds to allow you to easily setup + a ping/pong mechanism. +*/ +(function($){$.extend({bullet: function(url){ + var CONNECTING = 0; + var OPEN = 1; + var CLOSING = 2; + var CLOSED = 3; + + var transports = { + /** + The websocket transport is disabled for Firefox 6.0 because it + causes a crash to happen when the connection is closed. + @see https://bugzilla.mozilla.org/show_bug.cgi?id=662554 + */ + websocket: function(){ + var ret = false; + + if (window.WebSocket){ + ret = window.WebSocket; + } + + if (window.MozWebSocket + && navigator.userAgent.indexOf("Firefox/6.0") == -1){ + ret = window.MozWebSocket; + } + + if (ret){ + return {'heart': true, 'transport': ret}; + } + + return false; + }, + + xhrPolling: function(){ + var timeout; + var xhr; + + var fake = { + readyState: CONNECTING, + send: function(data){ + if (this.readyState != CONNECTING && this.readyState != OPEN){ + return false; + } + + var fakeurl = url.replace('ws:', 'http:').replace('wss:', 'https:'); + + $.ajax({ + async: false, + cache: false, + type: 'POST', + url: fakeurl, + data: data, + dataType: 'text', + contentType: + 'application/x-www-form-urlencoded; charset=utf-8', + headers: {'X-Socket-Transport': 'xhrPolling'}, + success: function(data){ + if (data.length != 0){ + fake.onmessage({'data': data}); + } + } + }); + + return true; + }, + close: function(){ + this.readyState = CLOSED; + xhr.abort(); + clearTimeout(timeout); + fake.onclose(); + }, + onopen: function(){}, + onmessage: function(){}, + onerror: function(){}, + onclose: function(){} + }; + + function poll(){ + var fakeurl = url.replace('ws:', 'http:').replace('wss:', 'https:'); + + xhr = $.ajax({ + type: 'GET', + cache: false, + url: fakeurl, + dataType: 'text', + data: {}, + headers: {'X-Socket-Transport': 'xhrPolling'}, + success: function(data){ + if (fake.readyState == CONNECTING){ + fake.readyState = OPEN; + fake.onopen(fake); + } + // Connection might have closed without a response body + if (data.length != 0){ + fake.onmessage({'data': data}); + } + if (fake.readyState == OPEN){ + nextPoll(); + } + }, + error: function(xhr){ + fake.onerror(); + } + }); + } + + function nextPoll(){ + timeout = setTimeout(function(){poll();}, 100); + } + + nextPoll(); + + return {'heart': false, 'transport': function(){ return fake; }}; + } + }; + + var tn = 0; + function next(){ + var c = 0; + + for (var f in transports){ + if (tn == c){ + var t = transports[f](); + if (t){ + var ret = new t.transport(url); + ret.heart = t.heart; + return ret; + } + + tn++; + } + + c++; + } + + return false; + } + + var stream = new function(){ + var readyState = CLOSED; + var heartbeat; + var delay = delayDefault = 80; + + var transport = next(); + function init(){ + readyState = CONNECTING; + + if (!transport){ + // No transport, give up + // @todo Trigger a disconnect error + return false; + } + + transport.onopen = function(){ + // We got a connection, reset the poll delay + delay = delayDefault; + + if (transport.heart){ + heartbeat = setInterval(function(){stream.onheartbeat();}, 20000); + } + + if (readyState != OPEN){ + readyState = OPEN; + stream.onopen(); + } + }; + transport.onclose = function(){ + clearInterval(heartbeat); + + if (readyState == CLOSING){ + readyState = CLOSED; + stream.onclose(); + } else{ + // Close happened on connect, select next transport + if (readyState == CONNECTING){ + tn++; + } + + delay *= 2; + if (delay > 10000){ + delay = 10000; + } + + setTimeout(function(){ + transport = next(); + init(); + }, delay); + } + }; + transport.onerror = transport.onclose; + transport.onmessage = function(e){ + stream.onmessage(e); + }; + } + init(); + + this.onopen = function(){}; + this.onmessage = function(){}; + this.onclose = function(){}; + this.onheartbeat = function(){}; + + this.setURL = function(newURL){ + url = newURL; + }; + this.send = function(data){ + return transport.send(data); + }; + this.close = function(){ + readyState = CLOSING; + transport.close(); + }; + }; + + return stream; +}})})(jQuery); diff --git a/rebar.config b/rebar.config index 239ae02..3936ca2 100644 --- a/rebar.config +++ b/rebar.config @@ -1,4 +1,5 @@ %%-*- mode: erlang -*- {deps, [ - {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "master"}} + {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "master"}}, + {bullet, ".*", {git, "https://github.com/extend/bullet.git", "master"}} ]}. diff --git a/src/cowboy_examples.erl b/src/cowboy_examples.erl index 75f411c..39a0a20 100644 --- a/src/cowboy_examples.erl +++ b/src/cowboy_examples.erl @@ -9,14 +9,22 @@ start() -> application:start(public_key), application:start(ssl), application:start(cowboy), + application:start(bullet), application:start(cowboy_examples). start(_Type, _Args) -> + Mimetypes = {mimetypes,[ + {<<".css">>, [<<"text/css">>]}, + {<<".js">>, [<<"application/javascript">>]}, + {<<".html">>, [<<"text/html">>]}]}, + StaticDir = {directory, <<"./priv/static">>}, Dispatch = [ {'_', [ {[<<"websocket">>], websocket_handler, []}, {[<<"eventsource">>], eventsource_handler, []}, {[<<"eventsource">>, <<"live">>], eventsource_emitter, []}, + {[<<"bullet">>], bullet_handler, [{handler, stream_handler}]}, + {[<<"priv">>, <<"static">>, '...'], cowboy_http_static, [StaticDir, Mimetypes]}, {'_', default_handler, []} ]} ], diff --git a/src/stream_handler.erl b/src/stream_handler.erl new file mode 100644 index 0000000..968a62e --- /dev/null +++ b/src/stream_handler.erl @@ -0,0 +1,17 @@ +-module(stream_handler). +-export([init/4, stream/3, info/3, terminate/2]). + +init(_Transport, Req, _Opts, Active) -> + io:format("INIT. Active: ~p~n", [Active]), + {ok, Req, undefined_state}. + +stream(Data, Req, State) -> + io:format("STREAM. Data: ~p~n", [Data]), + {reply, Data, Req, State}. + +info(_Info, Req, State) -> + {ok, Req, State}. + +terminate(_Req, _State) -> + io:format("TERMINATE.~n"), + ok. diff --git a/start.sh b/start.sh index d704f86..de17d6c 100755 --- a/start.sh +++ b/start.sh @@ -2,5 +2,6 @@ erl -sname cowboy_examples -pa ebin -pa deps/*/ebin -s cowboy_examples \ -eval "io:format(\"~n~nThe following examples are available:~n\")." \ -eval "io:format(\"* Hello world: http://localhost:8080~n\")." \ + -eval "io:format(\"* Bullet: http://localhost:8080/priv/static/bullet.html~n\")." \ -eval "io:format(\"* Websockets: http://localhost:8080/websocket~n\")." \ -eval "io:format(\"* Eventsource: http://localhost:8080/eventsource~n\")."