Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added really basic support for instruments.

  • Loading branch information...
commit 816f434d66b6fe7ec2b0f4a7a4c0ce404ec1dfc4 1 parent e3f7c5f
@toolness authored
View
2  server.js
@@ -9,6 +9,8 @@ var INDEX_FILE = '/index.html';
var STATIC_FILES = {
'/index.html': 'text/html'
, '/jquery.min.js': 'application/javascript'
+, '/jschannel.js': 'application/javascript'
+, '/example-instrument.html': 'text/html'
};
var server = http.createServer(function(req, res) {
View
36 static-files/example-instrument.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="author" content="Atul Varma">
+<title>Example Instrument</title>
+<!-- Date: 2010-10-16 -->
+<script src="/jschannel.js"></script>
+<script>
+var channel = Channel.build({
+ window: window.parent,
+ origin: '*',
+ scope: 'kitchen-party-instrument'
+});
+
+channel.bind('instrument:message', function(content) {
+ console.log("example instrument:message", content);
+});
+
+channel.bind('instrument:setState', function(state) {
+ console.log("example instrument:setState", state);
+ channel.call({
+ method: 'instrument:message',
+ params: [{oof: 'hi'}],
+ success: function() {},
+ error: function() {
+ console.error("example instrument:message failed.");
+ }
+ });
+});
+
+channel.call({
+ method: 'instrument:setSize',
+ params: [100, 100],
+ success: function() {}
+});
+</script>
+<p>html5 is cool.</p>
View
170 static-files/index.html
@@ -1,9 +1,11 @@
<!DOCTYPE html>
<meta charset="utf-8">
-<title>Hello</title>
+<title>Kitchen Party</title>
<script src="/socket.io/socket.io.js"></script>
+<script src="/jschannel.js"></script>
<script src="/jquery.min.js"></script>
<script>
+var COMMAND = /^\/([A-Za-z]+)(\s+(.*))?/;
var PUNCTUATION = /[.?!]$/;
var PUNCTUATION_MAP = {
'.': 'say',
@@ -23,10 +25,112 @@
$(document.body).scrollTop(height);
}
+function Instrument(id, url, state, send) {
+ var self = this;
+ var iframe = document.createElement('iframe');
+
+ // TODO: Outlaw other URLs? file:?
+ if (url.match(/^javascript/i))
+ throw new Error("unsafe URL: " + url);
+
+ iframe.src = url;
+ iframe.setAttribute("scrolling", "no");
+ $("#instruments").append(iframe);
+
+ var channel = Channel.build({
+ window: iframe.contentWindow,
+ // TODO: Only accept messages from original origin.
+ origin: '*',
+ scope: 'kitchen-party-instrument',
+ });
+
+ channel.bind('instrument:message', function(content) {
+ send({
+ msg: 'instrument:message',
+ id: id,
+ content: content
+ });
+ });
+
+ channel.bind('instrument:setState', function(state, broadcast) {
+ send({
+ msg: 'instrument:setState',
+ id: id,
+ state: state,
+ broadcast: broadcast
+ });
+ });
+
+ channel.bind('instrument:setSize', function(width, height) {
+ iframe.width = width;
+ iframe.height = height;
+ });
+
+ self.url = url;
+
+ self.message = function(content) {
+ channel.call({
+ method: 'instrument:message',
+ params: [content],
+ success: function() {},
+ error: function() {
+ console.error('instrument:message failed');
+ }
+ });
+ };
+
+ self.setState = function(state) {
+ channel.call({
+ method: 'instrument:setState',
+ params: state,
+ success: function() {},
+ error: function() {
+ console.log("instrument:setState failed");
+ }
+ });
+ }
+
+ self.remove = function() {
+ $(iframe).remove();
+ };
+
+ self.setState(state);
+ state = null;
+}
+
+function Instruments() {
+ var self = this;
+ var instruments = {};
+
+ self.add = function(id, url, state, send) {
+ instruments[id] = new Instrument(id, url, state, send);
+ };
+
+ self.remove = function(id) {
+ instruments[id].remove();
+ delete instruments[id];
+ };
+
+ self.get = function(id) {
+ return instruments[id];
+ };
+
+ self.getInfo = function() {
+ var info = [];
+ for (var id in instruments)
+ info.push({
+ id: id,
+ url: instruments[id].url
+ });
+ return info;
+ };
+}
+
$(window).ready(function() {
var socket = new io.Socket(window.location.hostname);
+ var instruments = new Instruments();
var id = null;
-
+
function send(obj) {
socket.send(JSON.stringify(obj));
}
@@ -43,6 +147,18 @@
}
var messageDelegate = {
+ 'instrument:add': function(options) {
+ instruments.add(options.id, options.url, options.state, send);
+ },
+ 'instrument:remove': function(options) {
+ instruments.remove(options.id);
+ },
+ 'instrument:message': function(options) {
+ instruments.get(options.id).message(options.content);
+ },
+ 'instrument:setState': function(options) {
+ instruments.get(options.id).setState(options.state);
+ },
init: function(options) {
id = options.id;
var init = $("#templates .init").clone();
@@ -54,6 +170,10 @@
fillName(present, userID);
addMessage(present);
});
+
+ options.instruments.forEach(function(options) {
+ instruments.add(options.id, options.url, options.state, send);
+ });
},
say: function(options) {
var match = options.content.match(PUNCTUATION);
@@ -97,12 +217,46 @@
});
socket.connect();
+ var commandDelegate = {
+ add: function(url) {
+ send({
+ msg: 'instrument:add',
+ url: url,
+ state: {}
+ });
+ },
+ remove: function(id) {
+ send({
+ msg: 'instrument:remove',
+ id: parseInt(id)
+ });
+ },
+ instruments: function() {
+ instruments.getInfo().forEach(function(info) {
+ var msg = $('<pre></pre>');
+ msg.text('instrument #' + info.id + ' @ ' + info.url);
+ addMessage(msg);
+ });
+ }
+ };
+
$("#prompt form").submit(function(event) {
event.preventDefault();
var input = $(this).find("input");
var content = input.val();
if (!content)
return;
+ var cmdMatch = content.match(COMMAND);
+ if (cmdMatch) {
+ var cmdName = cmdMatch[1];
+ var cmdArg = cmdMatch[3];
+ if (cmdName in commandDelegate) {
+ commandDelegate[cmdName](cmdArg);
+ input.val('');
+ } else
+ errorMessage('unknown-command');
+ return;
+ }
if (!content.match(PUNCTUATION)) {
errorMessage('bad-grammar');
return;
@@ -132,11 +286,21 @@
outline: none;
font-family: inherit;
font-size: inherit;
+ width: 90%;
}
.error {
color: firebrick;
}
+
+#instruments iframe {
+ border: none;
+}
+
+pre {
+ font-family: Monaco, "Lucida Console", monospace;
+ font-size: 9pt;
+}
</style>
<h1>Kitchen Party</h1>
<div id="messages">
@@ -144,10 +308,12 @@
<div id="prompt">
<form>&gt; <input type="text"></form>
</div>
+<div id="instruments"></div>
<div id="templates">
<span class="you">you</span>
<span class="other">number <span class="user-id"></span></span>
<div class="errors">
+ <div class="unknown-command">Unknown command.</div>
<div class="bad-grammar">Please use complete sentences.</div>
<div class="disconnected">You are disconnected.</div>
</div>
View
544 static-files/jschannel.js
@@ -0,0 +1,544 @@
+/**
+ * js_channel is a very lightweight abstraction on top of
+ * postMessage which defines message formats and semantics
+ * to support interactions more rich than just message passing
+ * js_channel supports:
+ * + query/response - traditional rpc
+ * + query/update/response - incremental async return of results
+ * to a query
+ * + notifications - fire and forget
+ * + error handling
+ *
+ * js_channel is based heavily on json-rpc, but is focused at the
+ * problem of inter-iframe RPC.
+ *
+ * Message types:
+ * There are 5 types of messages that can flow over this channel,
+ * and you may determine what type of message an object is by
+ * examining its parameters:
+ * 1. Requests
+ * + integer id
+ * + string method
+ * + (optional) any params
+ * 2. Callback Invocations (or just "Callbacks")
+ * + integer id
+ * + string callback
+ * + (optional) params
+ * 3. Error Responses (or just "Errors)
+ * + integer id
+ * + string error
+ * + (optional) string message
+ * 4. Responses
+ * + integer id
+ * + (optional) any result
+ * 5. Notifications
+ * + string method
+ * + (optional) any params
+ */
+
+;Channel = (function() {
+ // current transaction id, start out at a random *odd* number between 1 and a million
+ // There is one current transaction counter id per page, and it's shared between
+ // channel instances. That means of all messages posted from a single javascript
+ // evaluation context, we'll never have two with the same id.
+ var s_curTranId = Math.floor(Math.random()*1000001);
+
+ // no two bound channels in the same javascript evaluation context may have the same origin & scope.
+ // futher if two bound channels have the same scope, they may not have *overlapping* origins
+ // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
+ // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
+ // handlers. Request and Notification messages are routed using this table.
+ // Finally, channels are inserted into this table when built, and removed when destroyed.
+ var s_boundChans = { };
+
+ // add a channel to s_boundChans, throwing if a dup exists
+ function s_addBoundChan(origin, scope, handler) {
+ // does she exist?
+ var exists = false;
+ if (origin === '*') {
+ // we must check all other origins, sadly.
+ for (var k in s_boundChans) {
+ if (!s_boundChans.hasOwnProperty(k)) continue;
+ if (k === '*') continue;
+ if (typeof s_boundChans[k][scope] === 'object') {
+ exists = true;
+ }
+ }
+ } else {
+ // we must check only '*'
+ if ((s_boundChans['*'] && s_boundChans['*'][scope]) ||
+ (s_boundChans[origin] && s_boundChans[origin][scope]))
+ {
+ exists = true;
+ }
+ }
+ if (exists) throw "A channel already exists which overlaps with origin '"+ origin +"' and has scope '"+scope+"'";
+
+ if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
+ s_boundChans[origin][scope] = handler;
+ }
+
+ function s_removeBoundChan(origin, scope) {
+ delete s_boundChans[origin][scope];
+ // possibly leave a empty object around. whatevs.
+ }
+
+ // No two outstanding outbound messages may have the same id, period. Given that, a single table
+ // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
+ // Response messages. Entries are added to this table when requests are sent, and removed when
+ // responses are received.
+ var s_transIds = { };
+
+ // class singleton onMessage handler
+ // this function is registered once and all incoming messages route through here. This
+ // arrangement allows certain efficiencies, message data is only parsed once and dispatch
+ // is more efficient, especially for large numbers of simultaneous channels.
+ var s_onMessage = function(e) {
+ var m = JSON.parse(e.data);
+ if (typeof m !== 'object') return;
+
+ var o = e.origin;
+ var s = null;
+ var i = null;
+ var meth = null;
+
+ if (typeof m.method === 'string') {
+ var ar = m.method.split('::');
+ if (ar.length == 2) {
+ s = ar[0];
+ meth = ar[1];
+ } else {
+ meth = m.method;
+ }
+ }
+
+ if (typeof m.id !== 'undefined') i = m.id;
+
+ // o is message origin
+ // m is parsed message
+ // s is message scope
+ // i is message id (or null)
+ // meth is unscoped method name
+ // ^^ based on these factors we can route the message
+
+ // if it has a method it's either a notification or a request,
+ // route using s_boundChans
+ if (typeof meth === 'string') {
+ if (s_boundChans[o] && s_boundChans[o][s]) {
+ s_boundChans[o][s](o, meth, m);
+ } else if (s_boundChans['*'] && s_boundChans['*'][s]) {
+ s_boundChans['*'][s](o, meth, m);
+ }
+ }
+ // otherwise it must have an id (or be poorly formed
+ else if (typeof i != 'undefined') {
+ if (s_transIds[i]) s_transIds[i](o, meth, m);
+ }
+ };
+
+ // Setup postMessage event listeners
+ if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
+ else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage);
+
+ /* a messaging channel is constructed from a window and an origin.
+ * the channel will assert that all messages received over the
+ * channel match the origin
+ *
+ * Arguments to Channel.build(cfg):
+ *
+ * cfg.window - the remote window with which we'll communication
+ * cfg.origin - the expected origin of the remote window, may be '*'
+ * which matches any origin
+ * cfg.scope - the 'scope' of messages. a scope string that is
+ * prepended to message names. local and remote endpoints
+ * of a single channel must agree upon scope. Scope may
+ * not contain double colons ('::').
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.postMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these immediately
+ * before messages are posted.
+ * cfg.gotMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these arguments
+ * immediately after they pass scope and origin checks, but before
+ * they are processed.
+ * cfg.onReady - A function that will be invoked when a channel becomes "ready",
+ * this occurs once both sides of the channel have been
+ * instantiated and an application level handshake is exchanged.
+ * the onReady function will be passed a single argument which is
+ * the channel object that was returned from build().
+ */
+ return {
+ build: function(cfg) {
+ var debug = function(m) {
+ if (cfg.debugOutput && window.console && window.console.log) {
+ // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
+ try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { }
+ console.log("["+chanId+"] " + m);
+ }
+ }
+
+ /* browser capabilities check */
+ if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage");
+ if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
+ throw("jschannel cannot run this browser, no JSON parsing/serialization");
+ }
+
+ /* basic argument validation */
+ if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument");
+
+ if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument");
+
+ /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
+ * window... Not sure if we care to support that */
+ if (window === cfg.window) throw("target window is same as present window -- not allowed");
+
+ // let's require that the client specify an origin. if we just assume '*' we'll be
+ // propagating unsafe practices. that would be lame.
+ var validOrigin = false;
+ if (typeof cfg.origin === 'string') {
+ var oMatch;
+ if (cfg.origin === "*") validOrigin = true;
+ // allow valid domains under http and https. Also, trim paths off otherwise valid origins.
+ else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9\.])+(?::\d+)?/))) {
+ cfg.origin = oMatch[0];
+ validOrigin = true;
+ }
+ }
+
+ if (!validOrigin) throw ("Channel.build() called with an invalid origin");
+
+ if (typeof cfg.scope !== 'undefined') {
+ if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
+ if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"
+ }
+
+ /* private variables */
+ // generate a random and psuedo unique id for this channel
+ var chanId = (function () {
+ var text = "";
+ var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
+ return text;
+ })();
+
+ // registrations: mapping method names to call objects
+ var regTbl = { };
+ // current oustanding sent requests
+ var outTbl = { };
+ // current oustanding received requests
+ var inTbl = { };
+ // are we ready yet? when false we will block outbound messages.
+ var ready = false;
+ var pendingQueue = [ ];
+
+ var createTransaction = function(id,origin,callbacks) {
+ var shouldDelayReturn = false;
+ var completed = false;
+
+ return {
+ origin: origin,
+ invoke: function(cbName, v) {
+ // verify in table
+ if (!inTbl[id]) throw "attempting to invoke a callback of a non-existant transaction: " + id;
+ // verify that the callback name is valid
+ var valid = false;
+ for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
+ if (!valid) throw "request supports no such callback '" + cbName + "'";
+
+ // send callback invocation
+ postMessage({ id: id, callback: cbName, params: v});
+ },
+ error: function(error, message) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "error called for non-existant message: " + id;
+
+ // remove transaction from table
+ delete inTbl[id];
+
+ // send error
+ postMessage({ id: id, error: error, message: message });
+ },
+ complete: function(v) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "complete called for non-existant message: " + id;
+ // remove transaction from table
+ delete inTbl[id];
+ // send complete
+ postMessage({ id: id, result: v });
+ },
+ delayReturn: function(delay) {
+ if (typeof delay === 'boolean') {
+ shouldDelayReturn = (delay === true);
+ }
+ return shouldDelayReturn;
+ },
+ completed: function() {
+ return completed;
+ }
+ };
+ }
+
+ var onMessage = function(origin, method, m) {
+ // if an observer was specified at allocation time, invoke it
+ if (typeof cfg.gotMessageObserver === 'function') {
+ // pass observer a clone of the object so that our
+ // manipulations are not visible (i.e. method unscoping).
+ // This is not particularly efficient, but then we expect
+ // that message observers are primarily for debugging anyway.
+ try {
+ cfg.gotMessageObserver(origin, m);
+ } catch (e) {
+ debug("gotMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ // now, what type of message is this?
+ if (m.id && method) {
+ // a request! do we have a registered handler for this request?
+ if (regTbl[method]) {
+ var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]);
+ inTbl[m.id] = { };
+ try {
+ // callback handling. we'll magically create functions inside the parameter list for each
+ // callback
+ if (m.callbacks && m.callbacks instanceof Array && m.callbacks.length > 0) {
+ for (var i = 0; i < m.callbacks.length; i++) {
+ var path = m.callbacks[i];
+ var obj = m.params;
+ var pathItems = path.split('/');
+ for (var j = 0; j < pathItems.length - 1; j++) {
+ var cp = pathItems[j];
+ if (typeof obj[cp] !== 'object') obj[cp] = { };
+ obj = obj[cp];
+ }
+ obj[pathItems[pathItems.length - 1]] = (function() {
+ var cbName = path;
+ return function(params) {
+ return trans.invoke(cbName, params);
+ }
+ })();
+ }
+ }
+ var resp = regTbl[method](trans, m.params);
+ if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
+ } catch(e) {
+ // automagic handling of exceptions:
+ var error = "runtime_error";
+ var message = null;
+ // * if its a string then it gets an error code of 'runtime_error' and string is the message
+ if (typeof e === 'string') {
+ message = e;
+ } else if (typeof e === 'object') {
+ // either an array or an object
+ // * if its an array of length two, then array[0] is the code, array[1] is the error message
+ if (e && e instanceof Array && e.length == 2) {
+ error = e[0];
+ message = e[1];
+ }
+ // * if its an object then we'll look form error and message parameters
+ else if (typeof e.error === 'string') {
+ error = e.error;
+ if (!e.message) message = "";
+ else if (typeof e.message === 'string') message = e.message;
+ else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
+ }
+ }
+
+ // message is *still* null, let's try harder
+ if (message === null) {
+ try {
+ message = JSON.stringify(e);
+ } catch (e2) {
+ message = e.toString();
+ }
+ }
+
+ trans.error(error,message);
+ }
+ }
+ } else if (m.id && m.callback) {
+ if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
+ {
+ debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")");
+ } else {
+ // XXX: what if client code raises an exception here?
+ outTbl[m.id].callbacks[m.callback](m.params);
+ }
+ } else if (m.id && ((typeof m.result !== 'undefined') || m.error)) {
+ if (!outTbl[m.id]) {
+ debug("ignoring invalid response: " + m.id);
+ } else {
+ // XXX: what if client code raises an exception here?
+ if (m.error) {
+ outTbl[m.id].error(m.error, m.message);
+ } else {
+ outTbl[m.id].success(m.result);
+ }
+ delete outTbl[m.id];
+ delete s_transIds[m.id];
+ }
+ } else if (method) {
+ // tis a notification.
+ if (regTbl[method]) {
+ // yep, there's a handler for that.
+ // transaction is null for notifications.
+ regTbl[method](null, m.params);
+ // if the client throws, we'll just let it bubble out
+ // what can we do? Also, here we'll ignore return values
+ }
+ }
+ }
+
+ // now register our bound channel for msg routing
+ s_addBoundChan(cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
+
+ // scope method names based on cfg.scope specified when the Channel was instantiated
+ var scopeMethod = function(m) {
+ if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
+ return m;
+ }
+
+ // a small wrapper around postmessage whose primary function is to handle the
+ // case that clients start sending messages before the other end is "ready"
+ var postMessage = function(msg, force) {
+ if (!msg) throw "postMessage called with null message";
+
+ // delay posting if we're not ready yet.
+ var verb = (ready ? "post " : "queue ");
+ debug(verb + " message: " + JSON.stringify(msg));
+ if (!force && !ready) {
+ pendingQueue.push(msg);
+ } else {
+ if (typeof cfg.postMessageObserver === 'function') {
+ try {
+ cfg.postMessageObserver(cfg.origin, msg);
+ } catch (e) {
+ debug("postMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
+ }
+ }
+
+ var onReady = function(trans, type) {
+ debug('ready msg received');
+ if (ready) throw "received ready message while in ready state. help!";
+
+ if (type === 'ping') {
+ chanId += '-R';
+ } else {
+ chanId += '-L';
+ }
+
+ obj.unbind('__ready'); // now this handler isn't needed any more.
+ ready = true;
+ debug('ready msg accepted.');
+
+ if (type === 'ping') {
+ obj.notify({ method: '__ready', params: 'pong' });
+ }
+
+ // flush queue
+ while (pendingQueue.length) {
+ postMessage(pendingQueue.pop());
+ }
+
+ // invoke onReady observer if provided
+ if (typeof cfg.onReady === 'function') cfg.onReady(obj);
+ };
+
+ var obj = {
+ // tries to unbind a bound message handler. returns false if not possible
+ unbind: function (method) {
+ if (regTbl[method]) {
+ if (!(delete regTbl[method])) throw ("can't delete method: " + method);
+ return true;
+ }
+ return false;
+ },
+ bind: function (method, cb) {
+ if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
+ if (!cb || typeof cb !== 'function') throw "callback missing from bind params";
+
+ if (regTbl[method]) throw "method '"+method+"' is already bound!";
+ regTbl[method] = cb;
+ },
+ call: function(m) {
+ if (!m) throw 'missing arguments to call function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
+ if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
+
+ // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
+ // object and pick out all of the functions that were passed as arguments.
+ var callbacks = { };
+ var callbackNames = [ ];
+
+ var pruneFunctions = function (path, obj) {
+ if (typeof obj === 'object') {
+ for (var k in obj) {
+ if (!obj.hasOwnProperty(k)) continue;
+ var np = path + (path.length ? '/' : '') + k;
+ if (typeof obj[k] === 'function') {
+ callbacks[np] = obj[k];
+ callbackNames.push(np);
+ delete obj[k];
+ } else if (typeof obj[k] === 'object') {
+ pruneFunctions(np, obj[k]);
+ }
+ }
+ }
+ };
+ pruneFunctions("", m.params);
+
+ // build a 'request' message and send it
+ var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params };
+ if (callbackNames.length) msg.callbacks = callbackNames;
+
+ // insert into the transaction table
+ outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success };
+ s_transIds[s_curTranId] = onMessage;
+
+ // increment current id
+ s_curTranId++;
+
+ postMessage(msg);
+ },
+ notify: function(m) {
+ if (!m) throw 'missing arguments to notify function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
+
+ // no need to go into any transaction table
+ postMessage({ method: scopeMethod(m.method), params: m.params });
+ },
+ destroy: function () {
+ s_removeBoundChan(cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
+ if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
+ else if(window.detachEvent) window.detachEvent('onmessage', onMessage);
+ ready = false;
+ regTbl = { };
+ inTbl = { };
+ outTbl = { };
+ cfg.origin = null;
+ pendingQueue = [ ];
+ debug("channel destroyed");
+ chanId = "";
+ }
+ };
+
+ obj.bind('__ready', onReady);
+ setTimeout(function() {
+ postMessage({ method: scopeMethod('__ready'), params: "ping" }, true);
+ }, 0);
+
+ return obj;
+ }
+ };
+})();
View
72 users.js
@@ -8,7 +8,8 @@ function User(users, id, client) {
self.send({
msg: 'init',
id: id,
- users: users.get()
+ users: users.get(),
+ instruments: users.instruments.get()
});
client.on('message', function(data) {
@@ -22,6 +23,42 @@ function User(users, id, client) {
content: data.content
});
break;
+ case 'instrument:message':
+ users.broadcast({
+ msg: 'instrument:message',
+ from: id,
+ id: data.id,
+ content: data.content
+ });
+ break;
+ case 'instrument:setState':
+ users.instruments.setState(data.id, data.state);
+ if (data.broadcast)
+ users.broadcast({
+ msg: 'instrument:setState',
+ from: id,
+ id: data.id,
+ state: data.state
+ });
+ break;
+ case 'instrument:add':
+ var instrument = users.instruments.add(data.url, data.state);
+ users.broadcast({
+ msg: 'instrument:add',
+ from: id,
+ id: instrument.id,
+ url: instrument.url,
+ state: instrument.state
+ });
+ break;
+ case 'instrument:remove':
+ users.instruments.remove(data.id);
+ users.broadcast({
+ msg: 'instrument:remove',
+ from: id,
+ id: data.id
+ });
+ break;
default:
console.warn("Unknown msg:", data.msg);
break;
@@ -29,11 +66,44 @@ function User(users, id, client) {
});
}
+function Instruments() {
+ var self = this;
+ var instruments = {};
+ var latestID = 0;
+
+ self.get = function() {
+ var array = [];
+ for (var id in instruments)
+ array.push(instruments[id]);
+ return array;
+ };
+
+ self.setState = function(id, state) {
+ instruments[id].state = state;
+ };
+
+ self.add = function(url, state) {
+ var id = ++latestID;
+ instruments[id] = {
+ id: id,
+ url: url,
+ state: state
+ };
+ return instruments[id];
+ };
+
+ self.remove = function(id) {
+ delete instruments[id];
+ };
+}
+
function Users() {
var self = this;
var users = {};
var latestID = 0;
+ self.instruments = new Instruments();
+
self.onConnection = function(client) {
var id = ++latestID;
Please sign in to comment.
Something went wrong with that request. Please try again.