diff --git a/examples/nodeclient_example/chat.js b/examples/nodeclient_example/chat.js new file mode 100644 index 0000000..4f8125d --- /dev/null +++ b/examples/nodeclient_example/chat.js @@ -0,0 +1,16 @@ +var nowjs = require('../../lib/nodeclient/now.js'); +var now = nowjs.nowInitialize('http://localhost:8080'); +var readline = require('readline'); +var rl = readline.createInterface(process.stdin, process.stdout); +rl.on('line', function(line){ + now.distributeMessage(line); +}); +now.ready(function(){ + console.log("Chat server running!"); + rl.question("What's your name? ",function(answer){ + now.name = answer; + }); +}); +now.receiveMessage = function(message,name){ + console.log("----"+name+": "+message); +} diff --git a/examples/nodeclient_example/index.html b/examples/nodeclient_example/index.html new file mode 100644 index 0000000..48b94d1 --- /dev/null +++ b/examples/nodeclient_example/index.html @@ -0,0 +1,32 @@ + + + +nowjs test + + + + + + + +
+ + + + + diff --git a/examples/nodeclient_example/server.js b/examples/nodeclient_example/server.js new file mode 100644 index 0000000..ce94b0c --- /dev/null +++ b/examples/nodeclient_example/server.js @@ -0,0 +1,25 @@ +var http = require('http'); +var sys = require('util'); +var nowjs = require('now'); +var fs = require('fs'); +var server = http.createServer(function (req,res){ + fs.readFile('./index.html', function(error, content) { + if (error) { + res.writeHead(500); + res.end(); + } + else { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(content, 'utf-8'); + } + }); +}); +everyone = nowjs.initialize(server,{socketio: {"log level": 3}}); +everyone.now.log = function(str){ + console.log(str); +} +everyone.now.distributeMessage = function(str){ + everyone.now.receiveMessage(str,this.now.name); +} +server.listen(8080); +console.log("Listening on 8080"); diff --git a/lib/nodeclient/now.js b/lib/nodeclient/now.js new file mode 100644 index 0000000..e659ea7 --- /dev/null +++ b/lib/nodeclient/now.js @@ -0,0 +1,564 @@ + var nowObjects = {}; + var io = require('socket.io-client'); + var noConflict = function (uri, options) { + uri = uri || ''; + if (nowObjects[uri]) { + return nowObjects[uri]; + } + options = options || {}; + + var socket; + var closures = {}; + var nowReady = false; + var readied = 0; + var lastTimeout; + + var util, lib; + //uses the defineProperty function to test if browser isIE (this doesn't matter now) + var isIE = false; + + var fqnMap = { + data: {}, + arrays: {}, + get: function (fqn) { + return fqnMap.data[fqn]; + }, + //processes the input fqn for . notation, thereby signifying JS objects; if so, uses addParent and mutual recursion to set the children + set: function (fqn, val) { + if (fqnMap.data[fqn] !== undefined) { + fqnMap.deleteChildren(fqn, val); + } else { + var lastIndex = fqn.lastIndexOf('.'); + var parent = fqn.substring(0, lastIndex); + fqnMap.addParent(parent, fqn.substring(lastIndex + 1)); + } + return (fqnMap.data[fqn] = val); + }, + //checks if parent is not ''; if not, if an array has been created for parent, then it will add the key to that array + addParent: function (parent, key) { + if (parent) { + if (!util.isArray(fqnMap.data[parent])) { + fqnMap.set(parent, []); // Handle changing a non-object to an object. + } + fqnMap.data[parent].push(key); + } + }, + deleteChildren: function (fqn) { + var keys = this.data[fqn]; + var children = []; + if (util.isArray(this.data[fqn])) { + // Deleting a child will remove it via splice. + for (var i = 0; keys.length;) { + // Recursive delete all children. + var arr = this.deleteVar(fqn + '.' + keys[i]); + for (var j = 0; j < arr.length; j++) { + children.push(arr[j]); + } + } + } + return children; + }, + deleteVar: function (fqn) { + var lastIndex = fqn.lastIndexOf('.'); + var parent = fqn.substring(0, lastIndex); + if (util.hasProperty(this.data, parent)) { + var index = util.indexOf(this.data[parent], fqn.substring(lastIndex + 1)); + if (index > -1) { + this.data[parent].splice(index, 1); + } + } + var children = this.deleteChildren(fqn); + children.push(fqn); + delete this.data[fqn]; + this.unflagAsArray(fqn); + return children; + }, + flagAsArray: function (val) { + return (this.arrays[val] = true); + }, + unflagAsArray: function (val) { + delete this.arrays[val]; + } + }; + util = { + _events: {}, + // Event code from socket.io + on: function (name, fn) { + if (!(util.hasProperty(util._events, name))) { + util._events[name] = []; + } + util._events[name].push(fn); + return util; + }, + + indexOf: function (arr, val) { + for (var i = 0, ii = arr.length; i < ii; i++) { + if ("" + arr[i] === val) { + return i; + } + } + return -1; + }, + + emit: function (name, args) { + if (util.hasProperty(util._events, name)) { + var events = util._events[name].slice(0); + for (var i = 0, ii = events.length; i < ii; i++) { + events[i].apply(util, args === undefined ? [] : args); + } + } + return util; + }, + removeEvent: function (name, fn) { + if (util.hasProperty(util._events, name)) { + for (var a = 0, l = util._events[name].length; a < l; a++) { + if (util._events[name][a] === fn) { + util._events[name].splice(a, 1); + } + } + } + return util; + }, + + hasProperty: function (obj, prop) { + return Object.prototype.hasOwnProperty.call(Object(obj), prop); + }, + isArray: Array.isArray || function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }, + + createVarAtFqn: function (scope, fqn, value) { + var path = fqn.split('.'); + var currVar = util.forceGetParentVarAtFqn(scope, fqn); + var key = path.pop(); + fqnMap.set(fqn, (value && typeof value === 'object') ? [] : value); + if (util.isArray(value)) { + fqnMap.flagAsArray(fqn); + } + currVar[key] = value; + if (!(isIE || util.isArray(currVar))) { + util.watch(currVar, key, fqn); + } + }, + + forceGetParentVarAtFqn: function (scope, fqn) { + var path = fqn.split('.'); + path.shift(); + var currVar = scope; + while (path.length > 1) { + var prop = path.shift(); + if (!util.hasProperty(currVar, prop)) { + if (!isNaN(path[0])) { + currVar[prop] = []; + } else { + currVar[prop] = {}; + } + } + if(!(currVar[prop] && typeof currVar[prop] === "object")) { + currVar[prop] = {}; + } + currVar = currVar[prop]; + } + return currVar; + }, + + getVarFromFqn: function (scope, fqn) { + var path = fqn.split('.'); + path.shift(); + var currVar = scope; + while (path.length > 0) { + var prop = path.shift(); + if (util.hasProperty(currVar, prop)) { + currVar = currVar[prop]; + } else { + return false; + } + } + return currVar; + }, + + generateRandomString: function () { + return Math.random().toString().substr(2); + }, + + getValOrFqn: function (val, fqn) { + if (typeof val === 'function') { + if (val.remote) { + return undefined; + } + return {fqn: fqn}; + } else { + return val; + } + }, + + watch: function (obj, label, fqn) { + var val = obj[label]; + + function getter() { + return val; + } + function setter(newVal) { + if (val !== newVal && newVal !== fqnMap.get(fqn)) { + // trigger some sort of change. + if (val && typeof val === 'object') { + fqnMap.deleteVar(fqn); + socket.emit('del', [fqn]); + val = newVal; + lib.processScope(obj, fqn.substring(0, fqn.lastIndexOf('.'))); + return newVal; + } + if (newVal && typeof newVal === 'object') { + fqnMap.deleteVar(fqn); + socket.emit('del', [fqn]); + val = newVal; + lib.processScope(obj, fqn.substring(0, fqn.lastIndexOf('.'))); + return newVal; + } + fqnMap.set(fqn, newVal); + val = newVal; + if (typeof newVal === 'function') { + newVal = {fqn: fqn}; + } + var toReplace = {}; + toReplace[fqn] = newVal; + socket.emit('rv', toReplace); + } + return newVal; + } + + if (Object.defineProperty) { + Object.defineProperty(obj, label, {get: getter, set: setter}); + } else { + if (obj.__defineSetter__) { + obj.__defineSetter__(label, setter); + } + if (obj.__defineGetter__) { + obj.__defineGetter__(label, getter); + } + } + }, + + unwatch: function (obj, label) { + if (Object.defineProperty) { + Object.defineProperty(obj, label, {get: undefined, set: undefined}); + } else { + if (obj.__defineSetter__) { + obj.__defineSetter__(label, undefined); + } + if (obj.__defineGetter__) { + obj.__defineGetter__(label, undefined); + } + } + } + }; + + var now = { + ready: function (func) { + if (arguments.length === 0) { + util.emit('ready'); + } else { + if (nowReady) { + func(); + } + util.on('ready', func); + } + }, + core: { + on: util.on, + options: options, + removeEvent: util.removeEvent, + clientId: undefined, + noConflict: noConflict + } + }; + + lib = { + deleteVar: function (fqn) { + var path, currVar, parent, key; + path = fqn.split('.'); + currVar = now; + for (var i = 1; i < path.length; i++) { + key = path[i]; + if (currVar === undefined) { + // delete from fqnMap, just to be safe. + fqnMap.deleteVar(fqn); + return; + } + if (i === path.length - 1) { + delete currVar[path.pop()]; + fqnMap.deleteVar(fqn); + return; + } + currVar = currVar[key]; + } + }, + + replaceVar: function (data) { + for (var fqn in data) { + if (util.hasProperty(data[fqn], 'fqn')) { + data[fqn] = lib.constructRemoteFunction(fqn); + } + util.createVarAtFqn(now, fqn, data[fqn]); + } + }, + + remoteCall: function (data) { + var func; + // Retrieve the function, either from closures hash or from the now scope + if (data.fqn.split('_')[0] === 'closure') { + func = closures[data.fqn]; + } else { + func = util.getVarFromFqn(now, data.fqn); + } + var i, ii, args = data.args; + + if (typeof args === 'object' && !util.isArray(args)) { + var newargs = []; + // Enumeration order is not defined so this might be useless, + // but there will be cases when it works + for (i in args) { + newargs.push(args[i]); + } + args = newargs; + } + + // Search (only at top level) of args for functions parameters, + // and replace with wrapper remote call function + for (i = 0, ii = args.length; i < ii; i++) { + if (util.hasProperty(args[i], 'fqn')) { + args[i] = lib.constructRemoteFunction(args[i].fqn); + } + } + func.apply({now: now}, args); + }, + + // Handle the ready message from the server + serverReady: function () { + nowReady = true; + lib.processNowScope(); + util.emit('ready'); + }, + + constructRemoteFunction: function (fqn) { + var remoteFn = function () { + + lib.processNowScope(); + + var args = []; + for (var i = 0, ii = arguments.length; i < ii; i++) { + args[i] = arguments[i]; + } + for (i = 0, ii = args.length; i < ii; i++) { + if (typeof args[i] === 'function') { + var closureId = 'closure_' + args[i].name + '_' + util.generateRandomString(); + closures[closureId] = args[i]; + args[i] = {fqn: closureId}; + } + } + socket.emit('rfc', {fqn: fqn, args: args}); + }; + remoteFn.remote = true; + return remoteFn; + }, + handleNewConnection: function (socket) { + if (socket.handled) { + return; + } + socket.handled = true; + + socket.on('rfc', function (data) { + lib.remoteCall(data); + util.emit('rfc', data); + }); + socket.on('rv', function (data) { + lib.replaceVar(data); + util.emit('rv', data); + }); + socket.on('del', function (data) { + lib.deleteVar(data); + util.emit('del', data); + }); + + // Handle the ready message from the server + socket.on('rd', function (data) { + if (++readied === 2) { + lib.serverReady(); + } + }); + + socket.on('disconnect', function () { + readied = 0; + util.emit('disconnect'); + }); + // Forward planning for socket io 0.7 + socket.on('error', function () { + util.emit('error'); + }); + socket.on('retry', function () { + util.emit('retry'); + }); + socket.on('reconnect', function () { + util.emit('reconnect'); + }); + socket.on('reconnect_failed', function () { + util.emit('reconnect_failed'); + }); + socket.on('connect_failed', function () { + util.emit('connect_failed'); + }); + }, + processNowScope: function () { + lib.processScope(now, 'now'); + clearTimeout(lastTimeout); + if (socket.socket.connected) { + lastTimeout = setTimeout(lib.processNowScope, 1000); + } + }, + processScope: function (obj, path) { + var data = {}; + lib.traverseScope(obj, path, data); + // Send only for non-empty object + for (var i in data) { + if (util.hasProperty(data, i) && data[i] !== undefined) { + socket.emit('rv', data); + break; + } + } + }, + traverseScope: function (obj, path, data) { + + if (obj && typeof obj === 'object') { + var objIsArray = util.isArray(obj); + var keys = fqnMap.get(path); + for (var key in obj) { + var fqn = path + '.' + key; + + if (fqn === 'now.core' || fqn === 'now.ready') { + continue; + } + + if (util.hasProperty(obj, key)) { + + var val = obj[key]; + var mapVal = fqnMap.get(fqn); + var wasArray = fqnMap.arrays[fqn]; + var valIsArray = util.isArray(val); + var valIsObj = val && typeof val === 'object'; + var wasObject = util.isArray(mapVal) && !wasArray; + + if (objIsArray || isIE) { + if (valIsObj) { + if (valIsArray) { + // Value is an array + if (!wasArray) { + fqnMap.set(fqn, []); + fqnMap.flagAsArray(fqn); + data[fqn] = []; + } + } else { + // Value is object + if (!wasObject) { + fqnMap.set(fqn, []); + fqnMap.unflagAsArray(fqn); + data[fqn] = {}; + } + } + } else { + // Value is primitive / func + if (val !== mapVal) { + fqnMap.set(fqn, val); + fqnMap.unflagAsArray(fqn); + data[fqn] = util.getValOrFqn(val, fqn); + } + } + } else if (mapVal === undefined) { + util.watch(obj, key, fqn); + + if (valIsObj) { + if (valIsArray) { + // Value is array + fqnMap.set(fqn, []); + fqnMap.flagAsArray(fqn); + data[fqn] = []; + } else { + // Value is object + fqnMap.set(fqn, []); + data[fqn] = {}; + } + } else { + // Value is primitive / func + fqnMap.set(fqn, val); + data[fqn] = util.getValOrFqn(val, fqn); + } + } + if (valIsObj) { + lib.traverseScope(val, fqn, data); + } + } + } + + if (keys && typeof keys === 'object') { + var toDelete = []; + // Scan for deleted keys. + for (var i = 0; i < keys.length; i++) { + if (keys[i] !== undefined && obj[keys[i]] === undefined) { + toDelete.push(path + '.' + keys[i]); + fqnMap.deleteVar(path + '.' + keys[i]); + --i; + } + } + // Send message to server to delete from its database. + if (toDelete.length > 0) { + socket.emit('del', toDelete); + } + } + } + + }, + + }; + + var scriptLoaded = function () { + socket = io.connect(uri + '/', now.core.options.socketio || {}); + now.core.socketio = socket; + socket.on('connect', function () { + now.core.clientId = socket.socket.sessionid; + lib.handleNewConnection(socket); + // Begin intermittent scope traversal + + setTimeout(function () { + lib.processNowScope(); + socket.emit('rd'); + if (++readied === 2) { + nowReady = true; + util.emit('ready'); + } + }, 100); + + util.emit('connect'); + }); + socket.on('disconnect', function () { + // y-combinator trick + (function (y) { + y(y, now); + }(function (fn, obj) { + for (var i in obj) { + if (obj[i] && typeof obj[i] === 'object' && + obj[i] !== document && obj[i] !== now.core) { + fn(fn, obj[i]); + } + else if (typeof obj[i] === 'function' && obj[i].remote) { + delete obj[i]; + } + } + })); + // Clear all sorts of stuff in preparation for reconnecting. + fqnMap.data = {}; + }); + }; + scriptLoaded(); + return (nowObjects[uri] = now); + }; + + exports.nowInitialize = noConflict;