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\")."