summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--README.md38
-rw-r--r--erlang.mk168
-rw-r--r--examples/clock/src/clock.erl1
-rw-r--r--examples/clock/src/clock_app.erl7
-rw-r--r--examples/clock/src/toppage_handler.erl18
-rw-r--r--priv/bullet.js77
-rw-r--r--rebar.config2
-rw-r--r--src/bullet.app.src4
9 files changed, 239 insertions, 78 deletions
diff --git a/Makefile b/Makefile
index 0e5d78e..dcf8624 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ PROJECT = bullet
# Dependencies.
DEPS = cowboy
-dep_cowboy = https://github.com/extend/cowboy.git 0.8.4
+dep_cowboy = https://github.com/extend/cowboy.git 0.9.0
# Standard targets.
diff --git a/README.md b/README.md
index f2e357c..8822c68 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,19 @@ Bullet
Bullet is a Cowboy handler and associated Javascript library for
maintaining a persistent connection between a client and a server.
-Bullet abstracts a general transport protocol familiar to WebSockets, and
-is equipped with several "fallback" transports. Bullet will automatically
-use one of these when the browser used is not able to support WebSockets.
-
-A common interface is defined for both client and server-side to easily
+Bullet abstracts a general transport protocol similar to WebSockets.
+Bullet will use a WebSocket if possible but will fallback to other
+transports when necessary. If the client supports EventSource
+connections then Bullet will use EventSource to send messages from the
+server to the client and XHR for messages from the client to the
+server. If EventSource is not available then Bullet will use XHR for
+both directions, using long-polling for server-to-client messages.
+
+A common interface is defined for both client and server to easily
facilitate the handling of such connections. Bullet additionally takes care
of reconnecting automatically whenever a connection is lost, and also
provides an optional heartbeat which is managed on the client side.
-Today Bullet only supports websocket and long-polling transports.
-
Dispatch options
----------------
@@ -95,10 +97,13 @@ a document.ready function like this:
$(document).ready(function(){
var bullet = $.bullet('ws://localhost/path/to/bullet/handler');
bullet.onopen = function(){
- console.log('WebSocket: opened');
+ console.log('bullet: opened');
+ };
+ bullet.ondisconnect = function(){
+ console.log('bullet: disconnected');
};
bullet.onclose = function(){
- console.log('WebSocket: closed');
+ console.log('bullet: closed');
};
bullet.onmessage = function(e){
alert(e.data);
@@ -109,6 +114,21 @@ $(document).ready(function(){
});
```
+Always use the WebSocket (ws:) form for your bullet URLs and Bullet
+will change the URL as needed for non-WebSocket transports.
+
+The `$.bullet` function takes an optional second 'options' object.
+The following properties are supported:
+
+| Name | Default | Description |
+| ---------------------- | --------|------------------------------------ |
+| disableWebSocket | false | Never make WebSocket connections. |
+| disableEventSource | false | Never make EventSource connections. |
+| disableXHRPolling | false | Never fallback to XHR long polling. |
+
+Note that if EventSource is enabled and chosen as the underlying
+transport, XHR will be used for client-to-server messages.
+
Bullet works especially well when it is used to send JSON data
formatted with the jQuery JSON plugin.
diff --git a/erlang.mk b/erlang.mk
index d353c33..107cdd5 100644
--- a/erlang.mk
+++ b/erlang.mk
@@ -12,6 +12,21 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+# Project.
+
+PROJECT ?= $(notdir $(CURDIR))
+
+# Packages database file.
+
+PKG_FILE ?= $(CURDIR)/.erlang.mk.packages.v1
+export PKG_FILE
+
+PKG_FILE_URL ?= https://raw.github.com/extend/erlang.mk/master/packages.v1.tsv
+
+define get_pkg_file
+ wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE)
+endef
+
# Verbosity and tweaks.
V ?= 0
@@ -19,28 +34,71 @@ V ?= 0
appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src;
appsrc_verbose = $(appsrc_verbose_$(V))
-erlc_verbose_0 = @echo " ERLC " $(filter-out %.dtl,$(?F));
+erlc_verbose_0 = @echo " ERLC " $(filter %.erl %.core,$(?F));
erlc_verbose = $(erlc_verbose_$(V))
+xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F));
+xyrl_verbose = $(xyrl_verbose_$(V))
+
dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F));
dtl_verbose = $(dtl_verbose_$(V))
gen_verbose_0 = @echo " GEN " $@;
gen_verbose = $(gen_verbose_$(V))
-.PHONY: all clean-all app clean deps clean-deps docs clean-docs \
- build-tests tests build-plt dialyze
+.PHONY: rel clean-rel all clean-all app clean deps clean-deps \
+ docs clean-docs build-tests tests build-plt dialyze
+
+# Release.
+
+RELX_CONFIG ?= $(CURDIR)/relx.config
+
+ifneq ($(wildcard $(RELX_CONFIG)),)
+
+RELX ?= $(CURDIR)/relx
+export RELX
+
+RELX_URL ?= https://github.com/erlware/relx/releases/download/v0.5.2/relx
+RELX_OPTS ?=
+
+define get_relx
+ wget -O $(RELX) $(RELX_URL) || rm $(RELX)
+ chmod +x $(RELX)
+endef
+
+rel: clean-rel all $(RELX)
+ @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS)
+
+$(RELX):
+ @$(call get_relx)
+
+clean-rel:
+ @rm -rf _rel
+
+endif
# Deps directory.
DEPS_DIR ?= $(CURDIR)/deps
export DEPS_DIR
+REBAR_DEPS_DIR = $(DEPS_DIR)
+export REBAR_DEPS_DIR
+
ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS))
ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS))
# Application.
+ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),)
+ifeq ($(ERL_LIBS),)
+ ERL_LIBS = $(DEPS_DIR)
+else
+ ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)
+endif
+endif
+export ERL_LIBS
+
ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \
+warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec
COMPILE_FIRST ?=
@@ -52,19 +110,25 @@ clean-all: clean clean-deps clean-docs
$(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs
app: ebin/$(PROJECT).app
- $(eval MODULES := $(shell find ebin -name \*.beam \
+ $(eval MODULES := $(shell find ebin -type f -name \*.beam \
| sed 's/ebin\///;s/\.beam/,/' | sed '$$s/.$$//'))
$(appsrc_verbose) cat src/$(PROJECT).app.src \
- | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \
+ | sed 's/{modules,[[:space:]]*\[\]}/{modules, \[$(MODULES)\]}/' \
> ebin/$(PROJECT).app
define compile_erl
- $(erlc_verbose) ERL_LIBS=deps erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ \
- $(COMPILE_FIRST_PATHS) $(1)
+ $(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ \
+ -pa ebin/ -I include/ $(COMPILE_FIRST_PATHS) $(1)
+endef
+
+define compile_xyrl
+ $(xyrl_verbose) erlc -v -o ebin/ $(1)
+ $(xyrl_verbose) erlc $(ERLC_OPTS) -o ebin/ ebin/*.erl
+ @rm ebin/*.erl
endef
define compile_dtl
- $(dtl_verbose) erl -noshell -pa ebin/ deps/erlydtl/ebin/ -eval ' \
+ $(dtl_verbose) erl -noshell -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \
Compile = fun(F) -> \
Module = list_to_atom( \
string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \
@@ -74,10 +138,16 @@ define compile_dtl
init:stop()'
endef
-ebin/$(PROJECT).app: src/*.erl $(wildcard src/*.core) $(wildcard templates/*.dtl)
+ebin/$(PROJECT).app: $(shell find src -type f -name \*.erl) \
+ $(shell find src -type f -name \*.core) \
+ $(shell find src -type f -name \*.xrl) \
+ $(shell find src -type f -name \*.yrl) \
+ $(shell find templates -type f -name \*.dtl 2>/dev/null)
@mkdir -p ebin/
- $(if $(strip $(filter-out %.dtl,$?)), \
- $(call compile_erl,$(filter-out %.dtl,$?)))
+ $(if $(strip $(filter %.erl %.core,$?)), \
+ $(call compile_erl,$(filter %.erl %.core,$?)))
+ $(if $(strip $(filter %.xrl %.yrl,$?)), \
+ $(call compile_xyrl,$(filter %.xrl %.yrl,$?)))
$(if $(strip $(filter %.dtl,$?)), \
$(call compile_dtl,$(filter %.dtl,$?)))
@@ -88,7 +158,14 @@ clean:
define get_dep
@mkdir -p $(DEPS_DIR)
+ifeq (,$(findstring pkg://,$(word 1,$(dep_$(1)))))
git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1)
+else
+ @if [ ! -f $(PKG_FILE) ]; then $(call get_pkg_file); fi
+ git clone -n -- `awk 'BEGIN { FS = "\t" }; \
+ $$$$1 == "$(subst pkg://,,$(word 1,$(dep_$(1))))" { print $$$$2 }' \
+ $(PKG_FILE)` $(DEPS_DIR)/$(1)
+endif
cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1)))
endef
@@ -100,16 +177,30 @@ endef
$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep))))
deps: $(ALL_DEPS_DIRS)
- @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+ @for dep in $(ALL_DEPS_DIRS) ; do \
+ if [ -f $$dep/Makefile ] ; then \
+ $(MAKE) -C $$dep ; \
+ else \
+ echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep ; \
+ fi ; \
+ done
clean-deps:
- @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep clean; done
+ @for dep in $(ALL_DEPS_DIRS) ; do \
+ if [ -f $$dep/Makefile ] ; then \
+ $(MAKE) -C $$dep clean ; \
+ else \
+ echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep clean ; \
+ fi ; \
+ done
# Documentation.
+EDOC_OPTS ?=
+
docs: clean-docs
$(gen_verbose) erl -noshell \
- -eval 'edoc:application($(PROJECT), ".", []), init:stop().'
+ -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().'
clean-docs:
$(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
@@ -122,24 +213,39 @@ build-test-deps: $(ALL_TEST_DEPS_DIRS)
@for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
build-tests: build-test-deps
- $(gen_verbose) ERL_LIBS=deps erlc -v $(ERLC_OPTS) -o test/ \
+ $(gen_verbose) erlc -v $(ERLC_OPTS) -o test/ \
$(wildcard test/*.erl test/*/*.erl) -pa ebin/
CT_RUN = ct_run \
-no_auto_compile \
-noshell \
- -pa ebin $(DEPS_DIR)/*/ebin \
+ -pa $(realpath ebin) $(DEPS_DIR)/*/ebin \
-dir test \
-logdir logs
# -cover test/cover.spec
CT_SUITES ?=
-CT_SUITES_FULL = $(addsuffix _SUITE,$(CT_SUITES))
+
+define test_target
+test_$(1): ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}'
+test_$(1): clean deps app build-tests
+ @if [ -d "test" ] ; \
+ then \
+ mkdir -p logs/ ; \
+ $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) ; \
+ fi
+ $(gen_verbose) rm -f test/*.beam
+endef
+
+$(foreach test,$(CT_SUITES),$(eval $(call test_target,$(test))))
tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}'
tests: clean deps app build-tests
- @mkdir -p logs/
- @$(CT_RUN) -suite $(CT_SUITES_FULL)
+ @if [ -d "test" ] ; \
+ then \
+ mkdir -p logs/ ; \
+ $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) ; \
+ fi
$(gen_verbose) rm -f test/*.beam
# Dialyzer.
@@ -154,3 +260,27 @@ build-plt: deps app
dialyze:
@dialyzer --src src --plt .$(PROJECT).plt --no_native $(DIALYZER_OPTS)
+
+# Packages.
+
+$(PKG_FILE):
+ @$(call get_pkg_file)
+
+pkg-list: $(PKG_FILE)
+ @cat $(PKG_FILE) | awk 'BEGIN { FS = "\t" }; { print \
+ "Name:\t\t" $$1 "\n" \
+ "Repository:\t" $$2 "\n" \
+ "Website:\t" $$3 "\n" \
+ "Description:\t" $$4 "\n" }'
+
+ifdef q
+pkg-search: $(PKG_FILE)
+ @cat $(PKG_FILE) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \
+ "Name:\t\t" $$1 "\n" \
+ "Repository:\t" $$2 "\n" \
+ "Website:\t" $$3 "\n" \
+ "Description:\t" $$4 "\n" }'
+else
+pkg-search:
+ @echo "Usage: make pkg-search q=STRING"
+endif
diff --git a/examples/clock/src/clock.erl b/examples/clock/src/clock.erl
index e8547e1..f667565 100644
--- a/examples/clock/src/clock.erl
+++ b/examples/clock/src/clock.erl
@@ -22,5 +22,6 @@
start() ->
ok = application:start(crypto),
ok = application:start(ranch),
+ ok = application:start(cowlib),
ok = application:start(cowboy),
ok = application:start(clock).
diff --git a/examples/clock/src/clock_app.erl b/examples/clock/src/clock_app.erl
index 9b3985f..beaeb12 100644
--- a/examples/clock/src/clock_app.erl
+++ b/examples/clock/src/clock_app.erl
@@ -15,12 +15,7 @@ start(_Type, _Args) ->
{'_', [
{"/", toppage_handler, []},
{"/bullet", bullet_handler, [{handler, stream_handler}]},
- {"/static/[...]", cowboy_static, [
- {directory, {priv_dir, bullet, []}},
- {mimetypes, [
- {<<".js">>, [<<"application/javascript">>]}
- ]}
- ]}
+ {"/static/[...]", cowboy_static, {priv_dir, bullet, []}}
]}
]),
{ok, _} = cowboy:start_http(http, 100,
diff --git a/examples/clock/src/toppage_handler.erl b/examples/clock/src/toppage_handler.erl
index 552cb0e..71512d3 100644
--- a/examples/clock/src/toppage_handler.erl
+++ b/examples/clock/src/toppage_handler.erl
@@ -22,16 +22,20 @@ handle(Req, State) ->
<body>
<p><input type=\"checkbox\" checked=\"yes\" id=\"enable_best\"></input>
Current time (best source): <span id=\"time_best\">unknown</span>
- <span> </span><span id=\"status_best\">unknown</span></p>
+ <span></span><span id=\"status_best\">unknown</span>
+ <button id=\"send_best\">Send Time</button></p>
<p><input type=\"checkbox\" checked=\"yes\" id=\"enable_websocket\"></input>
Current time (websocket only): <span id=\"time_websocket\">unknown</span>
- <span> </span><span id=\"status_websocket\">unknown</span></p>
+ <span></span><span id=\"status_websocket\">unknown</span>
+ <button id=\"send_websocket\">Send Time</button></p>
<p><input type=\"checkbox\" checked=\"yes\" id=\"enable_eventsource\"></input>
Current time (eventsource only): <span id=\"time_eventsource\">unknown</span>
- <span> </span><span id=\"status_eventsource\">unknown</span></p>
+ <span></span><span id=\"status_eventsource\">unknown</span>
+ <button id=\"send_eventsource\">Send Time</button></p>
<p><input type=\"checkbox\" checked=\"yes\" id=\"enable_polling\"></input>
Current time (polling only): <span id=\"time_polling\">unknown</span>
- <span> </span><span id=\"status_polling\">unknown</span></p>
+ <span></span><span id=\"status_polling\">unknown</span>
+ <button id=\"send_polling\">Send Time</button></p>
<script
src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js\">
@@ -69,6 +73,12 @@ $(document).ready(function(){
bullet = null;
}
});
+ $('#send_' + name).on('click', function(){
+ if (bullet) {
+ bullet.send('time: ' + name + ' '
+ + $('#time_' + name).text());
+ }
+ });
};
start('best', {});
diff --git a/priv/bullet.js b/priv/bullet.js
index 1a53998..3d32679 100644
--- a/priv/bullet.js
+++ b/priv/bullet.js
@@ -38,6 +38,36 @@
var CLOSING = 2;
var CLOSED = 3;
+ var xhrSend = function(data){
+ /**
+ Send a message using ajax. Used for both the
+ eventsource and xhrPolling transports.
+ */
+ if (this.readyState != CONNECTING && this.readyState != OPEN){
+ return false;
+ }
+
+ var sendUrl = url.replace('ws:', 'http:').replace('wss:', 'https:');
+ var self = this;
+ $.ajax({
+ async: false,
+ cache: false,
+ type: 'POST',
+ url: sendUrl,
+ data: data,
+ dataType: 'text',
+ contentType: 'application/x-www-form-urlencoded; charset=utf-8',
+ headers: {'X-Socket-Transport': 'xhrPolling'},
+ success: function(data){
+ if (data && data.length !== 0){
+ self.onmessage({'data': data});
+ }
+ }
+ });
+
+ return true;
+ };
+
var transports = {
/**
The websocket transport is disabled for Firefox 6.0 because it
@@ -96,9 +126,7 @@
var fake = {
readyState: CONNECTING,
- send: function(data){
- return false; // fallback to another method instead?
- },
+ send: xhrSend,
close: function(){
fake.readyState = CLOSED;
source.close();
@@ -116,39 +144,17 @@
}
var timeout;
- var xhr;
+ var xhr = null;
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;
- },
+ send: xhrSend,
close: function(){
this.readyState = CLOSED;
- xhr.abort();
+ if (xhr){
+ xhr.abort();
+ xhr = null;
+ }
clearTimeout(timeout);
fake.onclose();
},
@@ -169,12 +175,13 @@
data: {},
headers: {'X-Socket-Transport': 'xhrPolling'},
success: function(data){
+ xhr = null;
if (fake.readyState == CONNECTING){
fake.readyState = OPEN;
fake.onopen(fake);
}
// Connection might have closed without a response body
- if (data.length != 0){
+ if (data && data.length !== 0){
fake.onmessage({'data': data});
}
if (fake.readyState == OPEN){
@@ -182,6 +189,7 @@
}
},
error: function(xhr){
+ xhr = null;
fake.onerror();
}
});
@@ -257,8 +265,9 @@
};
transport.onclose = function(){
// Firefox 13.0.1 sends 2 close events.
- // Return directly if we already handled it.
- if (isClosed){
+ // Return directly if we already handled it
+ // or we are closed
+ if (isClosed || readyState == CLOSED){
return;
}
diff --git a/rebar.config b/rebar.config
index 79e21c5..848f968 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,4 +1,4 @@
{deps, [
{cowboy, ".*",
- {git, "git://github.com/extend/cowboy.git", {tag, "0.8.4"}}}
+ {git, "git://github.com/extend/cowboy.git", {tag, "0.9.0"}}}
]}.
diff --git a/src/bullet.app.src b/src/bullet.app.src
index 4182009..366308f 100644
--- a/src/bullet.app.src
+++ b/src/bullet.app.src
@@ -13,14 +13,10 @@
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
{application, bullet, [
- {id, "Bullet"},
{description,
"Simple, reliable, efficient streaming for Cowboy."},
- {sub_description, "Bullet is a permanent bidirectional connection "
- "between the browser and the server."},
{vsn, "0.4.1"},
{modules, []},
- {registered, []},
{applications, [
kernel,
stdlib,