From a9d3e4eca4d1446555ba3084a449332dd0d79797 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Mon, 2 Dec 2013 20:41:19 -0500 Subject: [PATCH] Split out common drivers from chat example. --- Makefile | 6 +- examples/chat/index.html | 4 + examples/chat/index.js | 279 --------------------------------------- jquery-driver.js | 33 +++++ spy.js | 16 +++ wake-detector.js | 27 ++++ websocket-driver.js | 195 +++++++++++++++++++++++++++ 7 files changed, 280 insertions(+), 280 deletions(-) create mode 100644 jquery-driver.js create mode 100644 spy.js create mode 100644 wake-detector.js create mode 100644 websocket-driver.js diff --git a/Makefile b/Makefile index 3cdf03d..784b47f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ APP_NAME=MarketplaceChat.app LIB_SOURCES=\ - marketplace.js + marketplace.js \ + spy.js \ + jquery-driver.js \ + wake-detector.js \ + websocket-driver.js APP_SOURCES=\ examples/chat/index.html \ examples/chat/index.js \ diff --git a/examples/chat/index.html b/examples/chat/index.html index a5af6ba..273a261 100644 --- a/examples/chat/index.html +++ b/examples/chat/index.html @@ -11,6 +11,10 @@ + + + + diff --git a/examples/chat/index.js b/examples/chat/index.js index add5e23..5839670 100644 --- a/examples/chat/index.js +++ b/examples/chat/index.js @@ -29,285 +29,6 @@ function decodeAction(j) { } } -/////////////////////////////////////////////////////////////////////////// -// Generic Spy - -function Spy() { -} - -Spy.prototype.boot = function () { - World.updateRoutes([sub(__, 0, Infinity), pub(__, 0, Infinity)]); -}; - -Spy.prototype.handleEvent = function (e) { - switch (e.type) { - case "routes": console.log("SPY", "routes", e.routes); break; - case "message": console.log("SPY", "message", e.message, e.metaLevel, e.isFeedback); break; - default: console.log("SPY", "unknown", e); break; - } -}; - -/////////////////////////////////////////////////////////////////////////// -// JQuery event driver - -function spawnJQueryDriver() { - var d = new DemandMatcher(["jQuery", __, __, __]); - d.onDemandIncrease = function (r) { - var selector = r.pattern[1]; - var eventName = r.pattern[2]; - World.spawn(new JQueryEventRouter(selector, eventName), - [pub(["jQuery", selector, eventName, __]), - pub(["jQuery", selector, eventName, __], 0, 1)]); - }; - World.spawn(d); -} - -function JQueryEventRouter(selector, eventName) { - var self = this; - this.selector = selector; - this.eventName = eventName; - this.handler = - World.wrap(function (e) { - World.send(["jQuery", self.selector, self.eventName, e]); - e.preventDefault(); - return false; - }); - $(this.selector).on(this.eventName, this.handler); -} - -JQueryEventRouter.prototype.handleEvent = function (e) { - if (e.type === "routes" && e.routes.length === 0) { - $(this.selector).off(this.eventName, this.handler); - World.exit(); - } -}; - -/////////////////////////////////////////////////////////////////////////// -// Wake detector - notices when something (such as -// suspension/sleeping!) has caused periodic activities to be -// interrupted, and warns others about it -// Inspired by http://blog.alexmaccaw.com/javascript-wake-event - -function WakeDetector(period) { - this.message = "wake"; - this.period = period || 10000; - this.mostRecentTrigger = +(new Date()); - this.timerId = null; -} - -WakeDetector.prototype.boot = function () { - var self = this; - World.updateRoutes([pub(this.message)]); - this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period); -}; - -WakeDetector.prototype.handleEvent = function (e) {}; - -WakeDetector.prototype.trigger = function () { - var now = +(new Date()); - if (now - this.mostRecentTrigger > this.period * 1.5) { - World.send(this.message); - } - this.mostRecentTrigger = now; -}; - -/////////////////////////////////////////////////////////////////////////// -// WebSocket client driver - -var DEFAULT_RECONNECT_DELAY = 100; -var MAX_RECONNECT_DELAY = 30000; -var DEFAULT_IDLE_TIMEOUT = 300000; // 5 minutes -var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000; - -function WebSocketConnection(label, wsurl, shouldReconnect) { - this.label = label; - this.wsurl = wsurl; - this.shouldReconnect = shouldReconnect ? true : false; - this.reconnectDelay = DEFAULT_RECONNECT_DELAY; - this.localRoutes = []; - this.peerRoutes = []; - this.prevPeerRoutesMessage = null; - this.sock = null; - this.deduplicator = new Deduplicator(); - - this.activityTimestamp = 0; - this.idleTimeout = DEFAULT_IDLE_TIMEOUT; - this.pingInterval = DEFAULT_PING_INTERVAL; - this.idleTimer = null; - this.pingTimer = null; -} - -WebSocketConnection.prototype.clearHeartbeatTimers = function () { - if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; } - if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = null; } -}; - -WebSocketConnection.prototype.recordActivity = function () { - var self = this; - this.activityTimestamp = +(new Date()); - this.clearHeartbeatTimers(); - this.idleTimer = setTimeout(function () { self.forceclose(); }, - this.idleTimeout); - this.pingTimer = setTimeout(function () { self.safeSend(JSON.stringify("ping")) }, - this.pingInterval); -}; - -WebSocketConnection.prototype.statusRoute = function (status) { - return pub([this.label + "_state", status]); -}; - -WebSocketConnection.prototype.relayRoutes = function () { - // fresh copy each time, suitable for in-place extension/mutation - return [this.statusRoute(this.isConnected() ? "connected" : "disconnected"), - pub([this.label, __, __], 0, 1000), - sub([this.label, __, __], 0, 1000)]; -}; - -WebSocketConnection.prototype.aggregateRoutes = function () { - var rs = this.relayRoutes(); - for (var i = 0; i < this.peerRoutes.length; i++) { - var r = this.peerRoutes[i]; - rs.push(new Route(r.isSubscription, - [this.label, __, r.pattern], - r.metaLevel, - r.level)); - } - // console.log("WebSocketConnection.aggregateRoutes", this.label, rs); - return rs; -}; - -WebSocketConnection.prototype.boot = function () { - this.reconnect(); -}; - -WebSocketConnection.prototype.trapexit = function () { - this.forceclose(); -}; - -WebSocketConnection.prototype.isConnected = function () { - return this.sock && this.sock.readyState === this.sock.OPEN; -}; - -WebSocketConnection.prototype.safeSend = function (m) { - try { - if (this.isConnected()) { this.sock.send(m); } - } catch (e) { - console.warn("Trapped exn while sending", e); - } -}; - -WebSocketConnection.prototype.sendLocalRoutes = function () { - this.safeSend(JSON.stringify(encodeEvent(updateRoutes(this.localRoutes)))); -}; - -WebSocketConnection.prototype.handleEvent = function (e) { - // console.log("WebSocketConnection.handleEvent", e); - switch (e.type) { - case "routes": - this.localRoutes = []; - for (var i = 0; i < e.routes.length; i++) { - var r = e.routes[i]; - if (r.pattern.length && r.pattern.length === 3 - && r.pattern[0] === this.label - && typeof(r.pattern[1]) === "number") - { - this.localRoutes.push(new Route(r.isSubscription, - r.pattern[2], - r.pattern[1], - r.level)); - } - } - this.sendLocalRoutes(); - break; - case "message": - var m = e.message; - if (m.length && m.length === 3 - && m[0] === this.label - && typeof(m[1]) === "number") - { - var encoded = JSON.stringify(encodeEvent(sendMessage(m[2], m[1], e.isFeedback))); - if (this.deduplicator.accept(encoded)) { - this.safeSend(encoded); - } - } - break; - } -}; - -WebSocketConnection.prototype.forceclose = function (keepReconnectDelay) { - if (!keepReconnectDelay) { - this.reconnectDelay = DEFAULT_RECONNECT_DELAY; - } - this.clearHeartbeatTimers(); - if (this.sock) { - console.log("WebSocketConnection.forceclose called"); - this.sock.close(); - this.sock = null; - } -}; - -WebSocketConnection.prototype.reconnect = function () { - var self = this; - this.forceclose(true); - this.sock = new WebSocket(this.wsurl); - this.sock.onopen = World.wrap(function (e) { return self.onopen(e); }); - this.sock.onmessage = World.wrap(function (e) { return self.onmessage(e); }); - this.sock.onclose = World.wrap(function (e) { return self.onclose(e); }); -}; - -WebSocketConnection.prototype.onopen = function (e) { - console.log("connected to " + this.sock.url); - this.reconnectDelay = DEFAULT_RECONNECT_DELAY; - this.sendLocalRoutes(); -}; - -WebSocketConnection.prototype.onmessage = function (wse) { - // console.log("onmessage", wse); - this.recordActivity(); - - var j = JSON.parse(wse.data); - if (j === "ping") { - this.safeSend(JSON.stringify("pong")); - return; - } else if (j === "pong") { - return; // recordActivity already took care of our timers - } - - var e = decodeAction(j); - switch (e.type) { - case "routes": - if (this.prevPeerRoutesMessage !== wse.data) { - this.prevPeerRoutesMessage = wse.data; - this.peerRoutes = e.routes; - World.updateRoutes(this.aggregateRoutes()); - } - break; - case "message": - if (this.deduplicator.accept(wse.data)) { - World.send([this.label, e.metaLevel, e.message], 0, e.isFeedback); - } - break; - } -}; - -WebSocketConnection.prototype.onclose = function (e) { - var self = this; - console.log("onclose", e); - - // Update routes to give clients some indication of the discontinuity - World.updateRoutes(this.aggregateRoutes()); - - if (this.shouldReconnect) { - console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms"); - setTimeout(World.wrap(function () { self.reconnect(); }), this.reconnectDelay); - this.reconnectDelay = this.reconnectDelay * 1.618 + (Math.random() * 1000); - this.reconnectDelay = - this.reconnectDelay > MAX_RECONNECT_DELAY - ? MAX_RECONNECT_DELAY + (Math.random() * 1000) - : this.reconnectDelay; - } -}; - /////////////////////////////////////////////////////////////////////////// // Main diff --git a/jquery-driver.js b/jquery-driver.js new file mode 100644 index 0000000..954d5f7 --- /dev/null +++ b/jquery-driver.js @@ -0,0 +1,33 @@ +// JQuery event driver + +function spawnJQueryDriver() { + var d = new DemandMatcher(["jQuery", __, __, __]); + d.onDemandIncrease = function (r) { + var selector = r.pattern[1]; + var eventName = r.pattern[2]; + World.spawn(new JQueryEventRouter(selector, eventName), + [pub(["jQuery", selector, eventName, __]), + pub(["jQuery", selector, eventName, __], 0, 1)]); + }; + World.spawn(d); +} + +function JQueryEventRouter(selector, eventName) { + var self = this; + this.selector = selector; + this.eventName = eventName; + this.handler = + World.wrap(function (e) { + World.send(["jQuery", self.selector, self.eventName, e]); + e.preventDefault(); + return false; + }); + $(this.selector).on(this.eventName, this.handler); +} + +JQueryEventRouter.prototype.handleEvent = function (e) { + if (e.type === "routes" && e.routes.length === 0) { + $(this.selector).off(this.eventName, this.handler); + World.exit(); + } +}; diff --git a/spy.js b/spy.js new file mode 100644 index 0000000..d1295f9 --- /dev/null +++ b/spy.js @@ -0,0 +1,16 @@ +// Generic Spy + +function Spy() { +} + +Spy.prototype.boot = function () { + World.updateRoutes([sub(__, 0, Infinity), pub(__, 0, Infinity)]); +}; + +Spy.prototype.handleEvent = function (e) { + switch (e.type) { + case "routes": console.log("SPY", "routes", e.routes); break; + case "message": console.log("SPY", "message", e.message, e.metaLevel, e.isFeedback); break; + default: console.log("SPY", "unknown", e); break; + } +}; diff --git a/wake-detector.js b/wake-detector.js new file mode 100644 index 0000000..da11eb7 --- /dev/null +++ b/wake-detector.js @@ -0,0 +1,27 @@ +// Wake detector - notices when something (such as +// suspension/sleeping!) has caused periodic activities to be +// interrupted, and warns others about it +// Inspired by http://blog.alexmaccaw.com/javascript-wake-event + +function WakeDetector(period) { + this.message = "wake"; + this.period = period || 10000; + this.mostRecentTrigger = +(new Date()); + this.timerId = null; +} + +WakeDetector.prototype.boot = function () { + var self = this; + World.updateRoutes([pub(this.message)]); + this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period); +}; + +WakeDetector.prototype.handleEvent = function (e) {}; + +WakeDetector.prototype.trigger = function () { + var now = +(new Date()); + if (now - this.mostRecentTrigger > this.period * 1.5) { + World.send(this.message); + } + this.mostRecentTrigger = now; +}; diff --git a/websocket-driver.js b/websocket-driver.js new file mode 100644 index 0000000..758f974 --- /dev/null +++ b/websocket-driver.js @@ -0,0 +1,195 @@ +// WebSocket client driver + +var DEFAULT_RECONNECT_DELAY = 100; +var MAX_RECONNECT_DELAY = 30000; +var DEFAULT_IDLE_TIMEOUT = 300000; // 5 minutes +var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000; + +function WebSocketConnection(label, wsurl, shouldReconnect) { + this.label = label; + this.wsurl = wsurl; + this.shouldReconnect = shouldReconnect ? true : false; + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + this.localRoutes = []; + this.peerRoutes = []; + this.prevPeerRoutesMessage = null; + this.sock = null; + this.deduplicator = new Deduplicator(); + + this.activityTimestamp = 0; + this.idleTimeout = DEFAULT_IDLE_TIMEOUT; + this.pingInterval = DEFAULT_PING_INTERVAL; + this.idleTimer = null; + this.pingTimer = null; +} + +WebSocketConnection.prototype.clearHeartbeatTimers = function () { + if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; } + if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = null; } +}; + +WebSocketConnection.prototype.recordActivity = function () { + var self = this; + this.activityTimestamp = +(new Date()); + this.clearHeartbeatTimers(); + this.idleTimer = setTimeout(function () { self.forceclose(); }, + this.idleTimeout); + this.pingTimer = setTimeout(function () { self.safeSend(JSON.stringify("ping")) }, + this.pingInterval); +}; + +WebSocketConnection.prototype.statusRoute = function (status) { + return pub([this.label + "_state", status]); +}; + +WebSocketConnection.prototype.relayRoutes = function () { + // fresh copy each time, suitable for in-place extension/mutation + return [this.statusRoute(this.isConnected() ? "connected" : "disconnected"), + pub([this.label, __, __], 0, 1000), + sub([this.label, __, __], 0, 1000)]; +}; + +WebSocketConnection.prototype.aggregateRoutes = function () { + var rs = this.relayRoutes(); + for (var i = 0; i < this.peerRoutes.length; i++) { + var r = this.peerRoutes[i]; + rs.push(new Route(r.isSubscription, + [this.label, __, r.pattern], + r.metaLevel, + r.level)); + } + // console.log("WebSocketConnection.aggregateRoutes", this.label, rs); + return rs; +}; + +WebSocketConnection.prototype.boot = function () { + this.reconnect(); +}; + +WebSocketConnection.prototype.trapexit = function () { + this.forceclose(); +}; + +WebSocketConnection.prototype.isConnected = function () { + return this.sock && this.sock.readyState === this.sock.OPEN; +}; + +WebSocketConnection.prototype.safeSend = function (m) { + try { + if (this.isConnected()) { this.sock.send(m); } + } catch (e) { + console.warn("Trapped exn while sending", e); + } +}; + +WebSocketConnection.prototype.sendLocalRoutes = function () { + this.safeSend(JSON.stringify(encodeEvent(updateRoutes(this.localRoutes)))); +}; + +WebSocketConnection.prototype.handleEvent = function (e) { + // console.log("WebSocketConnection.handleEvent", e); + switch (e.type) { + case "routes": + this.localRoutes = []; + for (var i = 0; i < e.routes.length; i++) { + var r = e.routes[i]; + if (r.pattern.length && r.pattern.length === 3 + && r.pattern[0] === this.label + && typeof(r.pattern[1]) === "number") + { + this.localRoutes.push(new Route(r.isSubscription, + r.pattern[2], + r.pattern[1], + r.level)); + } + } + this.sendLocalRoutes(); + break; + case "message": + var m = e.message; + if (m.length && m.length === 3 + && m[0] === this.label + && typeof(m[1]) === "number") + { + var encoded = JSON.stringify(encodeEvent(sendMessage(m[2], m[1], e.isFeedback))); + if (this.deduplicator.accept(encoded)) { + this.safeSend(encoded); + } + } + break; + } +}; + +WebSocketConnection.prototype.forceclose = function (keepReconnectDelay) { + if (!keepReconnectDelay) { + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + } + this.clearHeartbeatTimers(); + if (this.sock) { + console.log("WebSocketConnection.forceclose called"); + this.sock.close(); + this.sock = null; + } +}; + +WebSocketConnection.prototype.reconnect = function () { + var self = this; + this.forceclose(true); + this.sock = new WebSocket(this.wsurl); + this.sock.onopen = World.wrap(function (e) { return self.onopen(e); }); + this.sock.onmessage = World.wrap(function (e) { return self.onmessage(e); }); + this.sock.onclose = World.wrap(function (e) { return self.onclose(e); }); +}; + +WebSocketConnection.prototype.onopen = function (e) { + console.log("connected to " + this.sock.url); + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + this.sendLocalRoutes(); +}; + +WebSocketConnection.prototype.onmessage = function (wse) { + // console.log("onmessage", wse); + this.recordActivity(); + + var j = JSON.parse(wse.data); + if (j === "ping") { + this.safeSend(JSON.stringify("pong")); + return; + } else if (j === "pong") { + return; // recordActivity already took care of our timers + } + + var e = decodeAction(j); + switch (e.type) { + case "routes": + if (this.prevPeerRoutesMessage !== wse.data) { + this.prevPeerRoutesMessage = wse.data; + this.peerRoutes = e.routes; + World.updateRoutes(this.aggregateRoutes()); + } + break; + case "message": + if (this.deduplicator.accept(wse.data)) { + World.send([this.label, e.metaLevel, e.message], 0, e.isFeedback); + } + break; + } +}; + +WebSocketConnection.prototype.onclose = function (e) { + var self = this; + console.log("onclose", e); + + // Update routes to give clients some indication of the discontinuity + World.updateRoutes(this.aggregateRoutes()); + + if (this.shouldReconnect) { + console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms"); + setTimeout(World.wrap(function () { self.reconnect(); }), this.reconnectDelay); + this.reconnectDelay = this.reconnectDelay * 1.618 + (Math.random() * 1000); + this.reconnectDelay = + this.reconnectDelay > MAX_RECONNECT_DELAY + ? MAX_RECONNECT_DELAY + (Math.random() * 1000) + : this.reconnectDelay; + } +};