diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | README.md | 38 | ||||
-rw-r--r-- | erlang.mk | 168 | ||||
-rw-r--r-- | examples/clock/src/clock.erl | 1 | ||||
-rw-r--r-- | examples/clock/src/clock_app.erl | 7 | ||||
-rw-r--r-- | examples/clock/src/toppage_handler.erl | 18 | ||||
-rw-r--r-- | priv/bullet.js | 77 | ||||
-rw-r--r-- | rebar.config | 2 | ||||
-rw-r--r-- | src/bullet.app.src | 4 |
9 files changed, 239 insertions, 78 deletions
@@ -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. @@ -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. @@ -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, |