From f363fb3bc718f8fc54bde85dbbcbf93f1ce88ee0 Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Tue, 28 Jan 2014 19:58:02 +0000 Subject: [PATCH 1/8] Inject yaffle's polyfill for IE eventsource --- javascripts/dashing.coffee | 1 + lib/dashing/app.rb | 11 + .../assets/javascripts/application.coffee | 6 +- .../project/assets/javascripts/eventsource.js | 474 ++++++++++++++++++ templates/project/dashboards/layout.erb | 2 +- 5 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 templates/project/assets/javascripts/eventsource.js diff --git a/javascripts/dashing.coffee b/javascripts/dashing.coffee index fa677e80..9c332d41 100644 --- a/javascripts/dashing.coffee +++ b/javascripts/dashing.coffee @@ -94,6 +94,7 @@ Dashing.debugMode = false source = new EventSource('events') source.addEventListener 'open', (e) -> + if Dashing.debugMode console.log("Connection opened", e) source.addEventListener 'error', (e)-> diff --git a/lib/dashing/app.rb b/lib/dashing/app.rb index 033849c7..0aa2e1b4 100644 --- a/lib/dashing/app.rb +++ b/lib/dashing/app.rb @@ -67,8 +67,19 @@ def protected! get '/events', provides: 'text/event-stream' do protected! response.headers['X-Accel-Buffering'] = 'no' # Disable buffering for nginx + response.headers['Access-Control-Allow-Origin'] = '*' # For Yaffle eventsource polyfill + response.headers['Cache-Control'] = 'no-cache' # For Yaffle eventsource polyfill + stream :keep_open do |out| settings.connections << out + + # For Yaffle eventsource polyfill + #Add 2k padding for IE + str = ":".ljust(2049) << "\n" + #add retry key + str << "retry: 2000\n" + out << str + out << latest_events out.callback { settings.connections.delete(out) } end diff --git a/templates/project/assets/javascripts/application.coffee b/templates/project/assets/javascripts/application.coffee index a1cbf3fc..da0f30d0 100644 --- a/templates/project/assets/javascripts/application.coffee +++ b/templates/project/assets/javascripts/application.coffee @@ -1,3 +1,7 @@ +# make sure Yaffle's eventsource goes in first +#= require eventsource.js + + # dashing.js is located in the dashing framework # It includes jquery & batman for you. #= require dashing.js @@ -5,7 +9,7 @@ #= require_directory . #= require_tree ../../widgets -console.log("Yeah! The dashboard has started!") +# console.log("Yeah! The dashboard has started!") Dashing.on 'ready', -> Dashing.widget_margins ||= [5, 5] diff --git a/templates/project/assets/javascripts/eventsource.js b/templates/project/assets/javascripts/eventsource.js new file mode 100644 index 00000000..428f9b68 --- /dev/null +++ b/templates/project/assets/javascripts/eventsource.js @@ -0,0 +1,474 @@ +/** + * eventsource.js + * Available under MIT License (MIT) + * https://github.com/Yaffle/EventSource/ + */ + +/*jslint indent: 2, vars: true, plusplus: true */ +/*global setTimeout, clearTimeout */ + +// console.log("Yaffle EventSource has been included the js asset"); + +(function (global) { + "use strict"; + + function Map() { + this.data = {}; + } + + Map.prototype = { + get: function (key) { + return this.data[key + "~"]; + }, + set: function (key, value) { + this.data[key + "~"] = value; + }, + "delete": function (key) { + delete this.data[key + "~"]; + } + }; + + function EventTarget() { + this.listeners = new Map(); + } + + function throwError(e) { + setTimeout(function () { + throw e; + }, 0); + } + + EventTarget.prototype = { + dispatchEvent: function (event) { + event.target = this; + var type = String(event.type); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (!typeListeners) { + return; + } + var length = typeListeners.length; + var i = -1; + var listener = null; + while (++i < length) { + listener = typeListeners[i]; + try { + listener.call(this, event); + } catch (e) { + throwError(e); + } + } + }, + addEventListener: function (type, callback) { + type = String(type); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (!typeListeners) { + typeListeners = []; + listeners.set(type, typeListeners); + } + var i = typeListeners.length; + while (--i >= 0) { + if (typeListeners[i] === callback) { + return; + } + } + typeListeners.push(callback); + }, + removeEventListener: function (type, callback) { + type = String(type); + var listeners = this.listeners; + var typeListeners = listeners.get(type); + if (!typeListeners) { + return; + } + var length = typeListeners.length; + var filtered = []; + var i = -1; + while (++i < length) { + if (typeListeners[i] !== callback) { + filtered.push(typeListeners[i]); + } + } + if (filtered.length === 0) { + listeners["delete"](type); + } else { + listeners.set(type, filtered); + } + } + }; + + function Event(type) { + this.type = type; + this.target = null; + } + + function MessageEvent(type, options) { + Event.call(this, type); + this.data = options.data; + this.lastEventId = options.lastEventId; + } + + MessageEvent.prototype = Event.prototype; + + var XHR = global.XMLHttpRequest; + var XDR = global.XDomainRequest; + var isCORSSupported = Boolean(XHR && ((new XHR()).withCredentials !== undefined)); + var isXHR = isCORSSupported; + var Transport = isCORSSupported ? XHR : XDR; + var WAITING = -1; + var CONNECTING = 0; + var OPEN = 1; + var CLOSED = 2; + var AFTER_CR = 3; + var FIELD_START = 4; + var FIELD = 5; + var VALUE_START = 6; + var VALUE = 7; + var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i; + + var MINIMUM_DURATION = 1000; + var MAXIMUM_DURATION = 18000000; + + function getDuration(value, def) { + var n = Number(value) || def; + return (n < MINIMUM_DURATION ? MINIMUM_DURATION : (n > MAXIMUM_DURATION ? MAXIMUM_DURATION : n)); + } + + function fire(that, f, event) { + try { + if (typeof f === "function") { + f.call(that, event); + } + } catch (e) { + throwError(e); + } + } + + function EventSource(url, options) { + url = String(url); + + // console.log("Created new Event Source"); + + var withCredentials = Boolean(isCORSSupported && options && options.withCredentials); + var initialRetry = getDuration(options ? options.retry : NaN, 1000); + var heartbeatTimeout = getDuration(options ? options.heartbeatTimeout : NaN, 45000); + var lastEventId = (options && options.lastEventId && String(options.lastEventId)) || ""; + var that = this; + var retry = initialRetry; + var wasActivity = false; + var xhr = new Transport(); + var timeout = 0; + var timeout0 = 0; + var charOffset = 0; + var currentState = WAITING; + var dataBuffer = []; + var lastEventIdBuffer = ""; + var eventTypeBuffer = ""; + var onTimeout = null; + + var state = FIELD_START; + var field = ""; + var value = ""; + + options = null; + + function close() { + currentState = CLOSED; + if (xhr !== null) { + xhr.abort(); + xhr = null; + } + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + if (timeout0 !== 0) { + clearTimeout(timeout0); + timeout0 = 0; + } + that.readyState = CLOSED; + } + + function onProgress(isLoadEnd) { + var responseText = currentState === OPEN || currentState === CONNECTING ? xhr.responseText || "" : ""; + var event = null; + var isWrongStatusCodeOrContentType = false; + + if (currentState === CONNECTING) { + var status = 0; + var statusText = ""; + var contentType = ""; + if (isXHR) { + try { + status = Number(xhr.status || 0); + statusText = String(xhr.statusText || ""); + contentType = String(xhr.getResponseHeader("Content-Type") || ""); + } catch (error) { + // https://bugs.webkit.org/show_bug.cgi?id=29121 + status = 0; + // FF < 14, WebKit + // https://bugs.webkit.org/show_bug.cgi?id=29658 + // https://bugs.webkit.org/show_bug.cgi?id=77854 + } + } else { + status = 200; + contentType = xhr.contentType; + } + if (status === 200 && contentTypeRegExp.test(contentType)) { + currentState = OPEN; + wasActivity = true; + retry = initialRetry; + that.readyState = OPEN; + event = new Event("open"); + that.dispatchEvent(event); + fire(that, that.onopen, event); + if (currentState === CLOSED) { + return; + } + } else { + if (status !== 0) { + var message = ""; + if (status !== 200) { + message = "EventSource's response has a status " + status + " " + statusText.replace(/\s+/g, " ") + " that is not 200. Aborting the connection."; + } else { + message = "EventSource's response has a Content-Type specifying an unsupported type: " + contentType.replace(/\s+/g, " ") + ". Aborting the connection."; + } + setTimeout(function () { + throw new Error(message); + }); + isWrongStatusCodeOrContentType = true; + } + } + } + + if (currentState === OPEN) { + if (responseText.length > charOffset) { + wasActivity = true; + } + var i = charOffset - 1; + var length = responseText.length; + var c = "\n"; + while (++i < length) { + c = responseText[i]; + if (state === AFTER_CR && c === "\n") { + state = FIELD_START; + } else { + if (state === AFTER_CR) { + state = FIELD_START; + } + if (c === "\r" || c === "\n") { + if (field === "data") { + dataBuffer.push(value); + } else if (field === "id") { + lastEventIdBuffer = value; + } else if (field === "event") { + eventTypeBuffer = value; + } else if (field === "retry") { + initialRetry = getDuration(value, initialRetry); + retry = initialRetry; + } else if (field === "heartbeatTimeout") {//! + heartbeatTimeout = getDuration(value, heartbeatTimeout); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = setTimeout(onTimeout, heartbeatTimeout); + } + } + value = ""; + field = ""; + if (state === FIELD_START) { + if (dataBuffer.length !== 0) { + lastEventId = lastEventIdBuffer; + if (eventTypeBuffer === "") { + eventTypeBuffer = "message"; + } + event = new MessageEvent(eventTypeBuffer, { + data: dataBuffer.join("\n"), + lastEventId: lastEventIdBuffer + }); + that.dispatchEvent(event); + if (eventTypeBuffer === "message") { + fire(that, that.onmessage, event); + } + if (currentState === CLOSED) { + return; + } + } + dataBuffer.length = 0; + eventTypeBuffer = ""; + } + state = c === "\r" ? AFTER_CR : FIELD_START; + } else { + if (state === FIELD_START) { + state = FIELD; + } + if (state === FIELD) { + if (c === ":") { + state = VALUE_START; + } else { + field += c; + } + } else if (state === VALUE_START) { + if (c !== " ") { + value += c; + } + state = VALUE; + } else if (state === VALUE) { + value += c; + } + } + } + } + charOffset = length; + } + + if ((currentState === OPEN || currentState === CONNECTING) && + (isLoadEnd || isWrongStatusCodeOrContentType || (charOffset > 1024 * 1024) || (timeout === 0 && !wasActivity))) { + currentState = WAITING; + xhr.abort(); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + if (retry > initialRetry * 16) { + retry = initialRetry * 16; + } + if (retry > MAXIMUM_DURATION) { + retry = MAXIMUM_DURATION; + } + timeout = setTimeout(onTimeout, retry); + retry = retry * 2 + 1; + + that.readyState = CONNECTING; + event = new Event("error"); + that.dispatchEvent(event); + fire(that, that.onerror, event); + } else { + if (timeout === 0) { + wasActivity = false; + timeout = setTimeout(onTimeout, heartbeatTimeout); + } + } + } + + function onProgress2() { + onProgress(false); + } + + function onLoadEnd() { + onProgress(true); + } + + if (isXHR) { + // workaround for Opera issue with "progress" events + timeout0 = setTimeout(function f() { + if (xhr.readyState === 3) { + onProgress2(); + } + timeout0 = setTimeout(f, 500); + }, 0); + } + + onTimeout = function () { + timeout = 0; + if (currentState !== WAITING) { + onProgress(false); + return; + } + // loading indicator in Safari, Chrome < 14, Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 + if (isXHR && (xhr.sendAsBinary !== undefined || xhr.onloadend === undefined) && global.document && global.document.readyState && global.document.readyState !== "complete") { + timeout = setTimeout(onTimeout, 4); + return; + } + // XDomainRequest#abort removes onprogress, onerror, onload + + xhr.onload = xhr.onerror = onLoadEnd; + + if (isXHR) { + // improper fix to match Firefox behaviour, but it is better than just ignore abort + // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 + // https://bugzilla.mozilla.org/show_bug.cgi?id=880200 + // https://code.google.com/p/chromium/issues/detail?id=153570 + xhr.onabort = onLoadEnd; + + // Firefox 3.5 - 3.6 - ? < 9.0 + // onprogress is not fired sometimes or delayed + xhr.onreadystatechange = onProgress2; + } + + xhr.onprogress = onProgress2; + + wasActivity = false; + timeout = setTimeout(onTimeout, heartbeatTimeout); + + charOffset = 0; + currentState = CONNECTING; + dataBuffer.length = 0; + eventTypeBuffer = ""; + lastEventIdBuffer = lastEventId; + value = ""; + field = ""; + state = FIELD_START; + + var s = url.slice(0, 5); + if (s !== "data:" && s !== "blob:") { + s = url + ((url.indexOf("?", 0) === -1 ? "?" : "&") + "lastEventId=" + encodeURIComponent(lastEventId) + "&r=" + String(Math.random() + 1).slice(2)); + } else { + s = url; + } + xhr.open("GET", s, true); + + if (isXHR) { + // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) + xhr.withCredentials = withCredentials; + + xhr.responseType = "text"; + + // Request header field Cache-Control is not allowed by Access-Control-Allow-Headers. + // "Cache-control: no-cache" are not honored in Chrome and Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=428916 + //xhr.setRequestHeader("Cache-Control", "no-cache"); + xhr.setRequestHeader("Accept", "text/event-stream"); + // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. + //xhr.setRequestHeader("Last-Event-ID", lastEventId); + } + + xhr.send(null); + }; + + EventTarget.call(this); + this.close = close; + this.url = url; + this.readyState = CONNECTING; + this.withCredentials = withCredentials; + + this.onopen = null; + this.onmessage = null; + this.onerror = null; + + onTimeout(); + } + + function F() { + this.CONNECTING = CONNECTING; + this.OPEN = OPEN; + this.CLOSED = CLOSED; + } + F.prototype = EventTarget.prototype; + + EventSource.prototype = new F(); + F.call(EventSource); + + if (Transport) { + // Why replace a native EventSource ? + // https://bugzilla.mozilla.org/show_bug.cgi?id=444328 + // https://bugzilla.mozilla.org/show_bug.cgi?id=831392 + // https://code.google.com/p/chromium/issues/detail?id=260144 + // https://code.google.com/p/chromium/issues/detail?id=225654 + // ... + global.NativeEventSource = global.EventSource; + global.EventSource = EventSource; + } + +}(this)); diff --git a/templates/project/dashboards/layout.erb b/templates/project/dashboards/layout.erb index eae97859..b33f20b8 100644 --- a/templates/project/dashboards/layout.erb +++ b/templates/project/dashboards/layout.erb @@ -4,7 +4,7 @@ - + <%= yield_content(:title) %> From f3a7b5d77e4639eb381a19ac6b87d865eb44b107 Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Tue, 28 Jan 2014 20:34:46 +0000 Subject: [PATCH 2/8] fix error in console log debug wrapper --- dashing.gemspec | 2 +- javascripts/dashing.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashing.gemspec b/dashing.gemspec index 8ef27f8f..a652c08b 100644 --- a/dashing.gemspec +++ b/dashing.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = 'dashing' - s.version = '1.3.2' + s.version = '1.3.x' s.date = '2013-11-21' s.executables << 'dashing' diff --git a/javascripts/dashing.coffee b/javascripts/dashing.coffee index 9c332d41..b392e4d2 100644 --- a/javascripts/dashing.coffee +++ b/javascripts/dashing.coffee @@ -95,7 +95,7 @@ Dashing.debugMode = false source = new EventSource('events') source.addEventListener 'open', (e) -> if Dashing.debugMode - console.log("Connection opened", e) + console.log("Connection opened", e) source.addEventListener 'error', (e)-> console.log("Connection error", e) From 2fe99eeb2a267534717e8077a37dd38c3b4744ee Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:04:11 +1000 Subject: [PATCH 3/8] hello numbers --- graphs/testing.json | 58 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 graphs/testing.json diff --git a/graphs/testing.json b/graphs/testing.json new file mode 100644 index 00000000..87a4492f --- /dev/null +++ b/graphs/testing.json @@ -0,0 +1,58 @@ +{ + "properties": { + "name": "testing", + "id": "testing", + "project": "dashing", + "environment": { + "type": "noflo-browser" + } + }, + "inports": {}, + "outports": {}, + "groups": [], + "processes": { + "math/Multiply_9jejf": { + "component": "math/Multiply", + "metadata": { + "label": "math/Multiply", + "x": 396, + "y": 180 + } + }, + "core/Output_g5k5i": { + "component": "core/Output", + "metadata": { + "label": "core/Output", + "x": 504, + "y": 180 + } + } + }, + "connections": [ + { + "src": { + "process": "math/Multiply_9jejf", + "port": "product" + }, + "tgt": { + "process": "core/Output_g5k5i", + "port": "in" + }, + "metadata": {} + }, + { + "data": 5, + "tgt": { + "process": "math/Multiply_9jejf", + "port": "multiplicand" + } + }, + { + "data": 4, + "tgt": { + "process": "math/Multiply_9jejf", + "port": "multiplier" + } + } + ] +} \ No newline at end of file From 20cd7aa889a07d94791c881a87da0469b9c190c1 Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:12:10 +1000 Subject: [PATCH 4/8] Update testing.json --- graphs/testing.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphs/testing.json b/graphs/testing.json index 87a4492f..cb8b8c62 100644 --- a/graphs/testing.json +++ b/graphs/testing.json @@ -41,18 +41,18 @@ "metadata": {} }, { - "data": 5, + "data": 25, "tgt": { "process": "math/Multiply_9jejf", "port": "multiplicand" } }, { - "data": 4, + "data": 24, "tgt": { "process": "math/Multiply_9jejf", "port": "multiplier" } } ] -} \ No newline at end of file +} From 1f739dabfcf46bf1934c7a183476d4f725f7545f Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:18:17 +1000 Subject: [PATCH 5/8] --- graphs/testing.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphs/testing.json b/graphs/testing.json index cb8b8c62..87a4492f 100644 --- a/graphs/testing.json +++ b/graphs/testing.json @@ -41,18 +41,18 @@ "metadata": {} }, { - "data": 25, + "data": 5, "tgt": { "process": "math/Multiply_9jejf", "port": "multiplicand" } }, { - "data": 24, + "data": 4, "tgt": { "process": "math/Multiply_9jejf", "port": "multiplier" } } ] -} +} \ No newline at end of file From 0867e8f450e1ef58300155cbc56657a569349262 Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:18:18 +1000 Subject: [PATCH 6/8] From 5f21d3bd4f402bb3bc5730e36dd7e95b9ad9794e Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:18:31 +1000 Subject: [PATCH 7/8] added component From 505521b30ffcb029cd6d8069d7f08b4e230c47e7 Mon Sep 17 00:00:00 2001 From: Dan Crawford Date: Sun, 9 Mar 2014 08:30:12 +1000 Subject: [PATCH 8/8] Delete testing.json --- graphs/testing.json | 58 --------------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 graphs/testing.json diff --git a/graphs/testing.json b/graphs/testing.json deleted file mode 100644 index 87a4492f..00000000 --- a/graphs/testing.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "properties": { - "name": "testing", - "id": "testing", - "project": "dashing", - "environment": { - "type": "noflo-browser" - } - }, - "inports": {}, - "outports": {}, - "groups": [], - "processes": { - "math/Multiply_9jejf": { - "component": "math/Multiply", - "metadata": { - "label": "math/Multiply", - "x": 396, - "y": 180 - } - }, - "core/Output_g5k5i": { - "component": "core/Output", - "metadata": { - "label": "core/Output", - "x": 504, - "y": 180 - } - } - }, - "connections": [ - { - "src": { - "process": "math/Multiply_9jejf", - "port": "product" - }, - "tgt": { - "process": "core/Output_g5k5i", - "port": "in" - }, - "metadata": {} - }, - { - "data": 5, - "tgt": { - "process": "math/Multiply_9jejf", - "port": "multiplicand" - } - }, - { - "data": 4, - "tgt": { - "process": "math/Multiply_9jejf", - "port": "multiplier" - } - } - ] -} \ No newline at end of file