Created
January 6, 2012 17:30
-
-
Save lezhangxyz/1571573 to your computer and use it in GitHub Desktop.
Now.js AMD client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(function() { | |
(function () { | |
var nowObjects = {}; | |
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; | |
var isIE = (function () { | |
try { | |
Object.defineProperty({}, '', {}); | |
return false; | |
} catch (err) { | |
if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
}()); | |
var fqnMap = { | |
data: {}, | |
arrays: {}, | |
get: function (fqn) { | |
return fqnMap.data[fqn]; | |
}, | |
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); | |
}, | |
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) { | |
//try { | |
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); | |
} | |
} | |
//} catch (e) {} | |
} | |
}; | |
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); | |
} | |
} | |
} | |
}, | |
traverseScopeIE: function (obj, path, data) { | |
} | |
}; | |
var dependencies = [ | |
{ key: 'io', path: '/socket.io/socket.io.js'} | |
]; | |
var dependenciesLoaded = 0; | |
var scriptLoaded = function () { | |
dependenciesLoaded++; | |
if (dependenciesLoaded !== dependencies.length) { | |
return; | |
} | |
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 = {}; | |
}); | |
}; | |
for (var i = 0, ii = dependencies.length; i < ii; i++) { | |
if (window[dependencies[i]['key']]) { | |
scriptLoaded(); | |
continue; | |
} | |
var fileref = document.createElement('script'); | |
fileref.setAttribute('type', 'text/javascript'); | |
fileref.setAttribute('src', uri + dependencies[i]['path']); | |
fileref.onload = scriptLoaded; | |
if (isIE) { | |
fileref.onreadystatechange = function () { | |
if (fileref.readyState === 'loaded' || fileref.readyState === 'complete') { | |
scriptLoaded(); | |
} | |
}; | |
} | |
document.getElementsByTagName('head')[0].appendChild(fileref); | |
} | |
return (nowObjects[uri] = now); | |
}; | |
window.nowInitialize = noConflict; | |
}()); | |
return nowInitialize("http://"+window.location.hostname, {}); | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment