1832 lines
77 KiB
JavaScript
1832 lines
77 KiB
JavaScript
/*global WeakMap, Map, Proxy, SockJS, Cereal, exports, require */
|
|
/*jslint browser: true, devel: true */
|
|
|
|
var Atomize;
|
|
|
|
function NotATVarException() {}
|
|
NotATVarException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: NotATVarException,
|
|
toString: function () {return "Not A TVar";}
|
|
};
|
|
|
|
function WriteOutsideTransactionException() {}
|
|
WriteOutsideTransactionException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: WriteOutsideTransactionException,
|
|
toString: function () {return "Write outside transaction";}
|
|
};
|
|
|
|
function DeleteOutsideTransactionException() {}
|
|
DeleteOutsideTransactionException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: DeleteOutsideTransactionException,
|
|
toString: function () {return "Delete outside transaction";}
|
|
};
|
|
|
|
function RetryOutsideTransactionException() {}
|
|
RetryOutsideTransactionException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: RetryOutsideTransactionException,
|
|
toString: function () {return "Retry outside transaction";}
|
|
};
|
|
|
|
function InternalException() {}
|
|
InternalException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: InternalException,
|
|
toString: function () {return "Internal Exception";}
|
|
};
|
|
|
|
function UnpopulatedException(tvar) { this.tvar = tvar; }
|
|
UnpopulatedException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: UnpopulatedException,
|
|
toString: function () {return "Use of unpopulated tvar: " + this.tvar.id;}
|
|
};
|
|
|
|
function AccessorDescriptorsNotSupportedException(desc) { this.desc = desc; }
|
|
AccessorDescriptorsNotSupportedException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: AccessorDescriptorsNotSupportedException,
|
|
toString: function () {
|
|
return "Accessor Descriptors Not Supported: " +
|
|
JSON.stringify(this.desc);
|
|
}
|
|
};
|
|
|
|
function InvalidDescriptorException(desc) { this.desc = desc; }
|
|
InvalidDescriptorException.prototype = {
|
|
prototype: Error.prototype,
|
|
constructor: InvalidDescriptorException,
|
|
toString: function () {
|
|
return "Invalid Descriptor: " + JSON.stringify(this.desc);
|
|
}
|
|
};
|
|
|
|
|
|
(function (window) {
|
|
'use strict';
|
|
|
|
var util, STM, Cereal, events;
|
|
|
|
if (typeof exports !== "undefined") {
|
|
Cereal = require('cereal');
|
|
events = require('events');
|
|
|
|
} else {
|
|
Cereal = window.Cereal;
|
|
|
|
if (typeof Cereal === "undefined") {
|
|
console.error("Please load the cereal.js script before the atomize.js script.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
util = (function () {
|
|
var result, i, name, names;
|
|
|
|
result = {};
|
|
names = ['defineProperty', 'getPropertyDescriptor',
|
|
'getPropertyNames', 'getOwnPropertyDescriptor',
|
|
'getOwnPropertyNames', 'keys', 'hasOwnProperty'];
|
|
|
|
for (i = 0; i < names.length; i += 1) {
|
|
name = names[i];
|
|
if (undefined !== Object[name]) {
|
|
if (undefined === Object[name]._origFun) {
|
|
result[name] = Object[name];
|
|
} else {
|
|
result[name] = Object[name]._origFun;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.isPrimitive = function (obj) {
|
|
return obj !== Object(obj);
|
|
};
|
|
|
|
result.hasOwnProp = result.hasOwnProperty;
|
|
|
|
result.shallowCopy = function (src, dest) {
|
|
var keys, i;
|
|
keys = result.keys(src);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
dest[keys[i]] = src[keys[i]];
|
|
}
|
|
};
|
|
|
|
result.lift = function (src, dest, fields) {
|
|
var i;
|
|
for (i = 0; i < fields.length; i += 1) {
|
|
dest[fields[i]] = src[fields[i]].bind(src);
|
|
}
|
|
};
|
|
|
|
(function () {
|
|
var nextId = 0, MyMap;
|
|
if (typeof Map === "undefined") {
|
|
if (typeof WeakMap === "undefined") {
|
|
MyMap = function () {
|
|
this.objs = {};
|
|
this.prims = {};
|
|
};
|
|
MyMap.prototype = {
|
|
id: function () {
|
|
nextId += 1;
|
|
return nextId;
|
|
},
|
|
|
|
set: function (key, value) {
|
|
if (result.isPrimitive(key)) {
|
|
this.prims[key] = value;
|
|
} else {
|
|
if (! result.hasOwnProp.call(key, '_map')) {
|
|
result.defineProperty(
|
|
key, '_map',
|
|
{value: this.id(),
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: false});
|
|
}
|
|
this.objs[key._map] = value;
|
|
}
|
|
},
|
|
|
|
get: function (key) {
|
|
if (result.isPrimitive(key)) {
|
|
return this.prims[key];
|
|
} else {
|
|
if (result.hasOwnProp.call(key, '_map')) {
|
|
return this.objs[key._map];
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
},
|
|
|
|
has: function (key) {
|
|
if (result.isPrimitive(key)) {
|
|
return result.hasOwnProp.call(this.prims, key);
|
|
} else {
|
|
return (result.hasOwnProp.call(key, '_map') &&
|
|
result.hasOwnProp.call(this.objs, key._map));
|
|
}
|
|
}
|
|
};
|
|
} else {
|
|
MyMap = WeakMap;
|
|
}
|
|
} else {
|
|
MyMap = Map;
|
|
}
|
|
result.Map = MyMap;
|
|
}());
|
|
|
|
return result;
|
|
}());
|
|
|
|
(function () {
|
|
|
|
function TVar(id, obj, stm) {
|
|
this.id = id;
|
|
this.raw = obj;
|
|
this.isArray = Array === obj.constructor;
|
|
this.stm = stm;
|
|
this.version = 0;
|
|
this.handler = this.objHandlerMaker();
|
|
this.proxied = this.createProxy(this.handler, Object.getPrototypeOf(obj));
|
|
}
|
|
|
|
TVar.prototype = {
|
|
unpopulated: false,
|
|
|
|
hasNativeProxy: "undefined" !== typeof Proxy,
|
|
|
|
log: function () {
|
|
var args;
|
|
if (this.stm.isLogging) {
|
|
args = Array.prototype.slice.call(arguments, 0);
|
|
args.unshift("[TVar " + this.id + "]");
|
|
this.stm.log.apply(this.stm, args);
|
|
}
|
|
},
|
|
|
|
clone: function (obj) {
|
|
// provided the .proxy is different from the .raw then
|
|
// we're ok. But in a few places we also need to make
|
|
// sure the prototype of the .proxy matches the
|
|
// prototype of the .raw (in order to correctly walk
|
|
// down the prototype chain), hence this method.
|
|
var result, keys, i, ctr = function () {};
|
|
ctr.prototype = Object.getPrototypeOf(obj);
|
|
result = new ctr();
|
|
util.shallowCopy(obj, result);
|
|
return result;
|
|
},
|
|
|
|
createProxy: function (handler, proto) {
|
|
if (this.hasNativeProxy) {
|
|
return Proxy.create(handler, proto);
|
|
} else {
|
|
return this.clone(this.raw);
|
|
}
|
|
},
|
|
|
|
objHandlerMaker: function () {
|
|
var self, stm, handler;
|
|
|
|
self = this;
|
|
stm = self.stm;
|
|
|
|
handler = {
|
|
|
|
getOwnPropertyDescriptor: function (name) {
|
|
self.log("getOwnPropertyDescriptor:", name);
|
|
if ("_map" !== name) {
|
|
stm.recordRead(self);
|
|
}
|
|
var desc, value;
|
|
if ("_map" === name || ! stm.inTransaction()) {
|
|
desc = util.getOwnPropertyDescriptor(self.raw, name);
|
|
} else if (stm.transactionFrame.isDeleted(self, name)) {
|
|
return undefined;
|
|
} else {
|
|
desc = stm.transactionFrame.get(self, name);
|
|
if (undefined === desc) {
|
|
desc = util.getOwnPropertyDescriptor(self.raw, name);
|
|
if (undefined !== desc &&
|
|
! (undefined === desc.value ||
|
|
util.isPrimitive(desc.value) ||
|
|
'function' === typeof desc.value)) {
|
|
value = stm.ensureTVar(desc.value).proxied;
|
|
if (desc.value !== value) {
|
|
// rewrite our local graph to use the proxied version
|
|
self.log("Implicity lifting");
|
|
desc.value = value;
|
|
stm.transactionFrame.recordDefine(self, name, desc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (undefined !== desc &&
|
|
util.hasOwnProp.call(desc, 'configurable')) {
|
|
desc.configurable = true; // should go away with direct proxies
|
|
}
|
|
return desc;
|
|
},
|
|
|
|
getPropertyDescriptor: function (name) {
|
|
self.log("getPropertyDescriptor:", name);
|
|
if ("_map" !== name) {
|
|
stm.recordRead(self);
|
|
}
|
|
var visited = [], tvar = self, obj = tvar.proxied, desc;
|
|
while (obj !== undefined && obj !== null) {
|
|
tvar = stm.ensureTVar(obj);
|
|
if (-1 !== visited.lastIndexOf(tvar)) {
|
|
break;
|
|
}
|
|
obj = tvar.proxied;
|
|
desc = tvar.handler.getOwnPropertyDescriptor(name);
|
|
if (undefined === desc) {
|
|
visited.push(tvar);
|
|
obj = Object.getPrototypeOf(obj);
|
|
} else {
|
|
return desc;
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
|
|
getOwnPropertyNames: function () {
|
|
self.log("getOwnPropertyNames");
|
|
stm.recordRead(self);
|
|
var names, result, i;
|
|
if (stm.inTransaction()) {
|
|
result = {};
|
|
// names here won't contain any names that have been deleted...
|
|
names = stm.transactionFrame.getOwnPropertyNames(self);
|
|
for (i = 0; i < names.length; i += 1) {
|
|
result[names[i]] = true;
|
|
}
|
|
// ...but here, these names may have been deleted in the txn.
|
|
names = util.getOwnPropertyNames(self.raw);
|
|
for (i = 0; i < names.length; i += 1) {
|
|
if (! stm.transactionFrame.isDeleted(self, names[i])) {
|
|
result[names[i]] = true;
|
|
}
|
|
}
|
|
return util.keys(result);
|
|
} else {
|
|
return util.getOwnPropertyNames(self.raw);
|
|
}
|
|
},
|
|
|
|
getPropertyNames: function () {
|
|
self.log("getPropertyNames");
|
|
stm.recordRead(self);
|
|
var seen = {}, visited = [], tvar = self, obj = tvar.proxied, names, i;
|
|
// the final Object.prototype !== obj is probably a bug in chrome/v8:
|
|
// http://code.google.com/p/v8/issues/detail?id=2145
|
|
while (obj !== undefined && obj !== null && Object.prototype !== obj) {
|
|
tvar = stm.ensureTVar(obj);
|
|
if (-1 !== visited.lastIndexOf(tvar)) {
|
|
break;
|
|
}
|
|
obj = tvar.proxied;
|
|
names = tvar.handler.getOwnPropertyNames();
|
|
for (i = 0; i < names.length; i += 1) {
|
|
if (! util.hasOwnProp.call(seen, names[i])) {
|
|
seen[names[i]] = true;
|
|
}
|
|
}
|
|
visited.push(tvar);
|
|
obj = Object.getPrototypeOf(obj);
|
|
}
|
|
return util.keys(seen);
|
|
},
|
|
|
|
defineProperty: function (name, desc) {
|
|
var current, match, key, merged;
|
|
self.log("defineProperty:", name);
|
|
if ("_map" === name) {
|
|
return util.defineProperty(self.raw, name, desc);
|
|
}
|
|
if (stm.inTransaction()) {
|
|
if (util.hasOwnProp.call(desc, 'get') ||
|
|
util.hasOwnProp.call(desc, 'set')) {
|
|
throw new AccessorDescriptorsNotSupportedException(desc);
|
|
} else {
|
|
if ('value' in desc &&
|
|
(!util.isPrimitive(desc.value)) &&
|
|
(!stm.isProxied(desc.value))) {
|
|
// the value is not a tvar, explode
|
|
throw new NotATVarException(desc.value);
|
|
}
|
|
current = handler.getOwnPropertyDescriptor(name);
|
|
if (undefined === current) {
|
|
if (Object.isExtensible(self.proxied)) {
|
|
// fill in the default values
|
|
desc.writable = desc.writable || false;
|
|
desc.enumerable = desc.enumerable || false;
|
|
desc.configurable = desc.configurable || false;
|
|
desc.value = 'value' in desc ? desc.value : undefined;
|
|
stm.transactionFrame.recordDefine(self, name, desc);
|
|
return self.proxied;
|
|
} else {
|
|
throw new InvalidDescriptorException(desc);
|
|
}
|
|
}
|
|
match = true;
|
|
for (key in desc) {
|
|
if (desc[key] !== current[key]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
return self.proxied;
|
|
}
|
|
if ((! current.configurable) &&
|
|
(desc.configurable ||
|
|
('enumerable' in desc && desc.enumerable !== current.enumerable))) {
|
|
throw new InvalidDescriptorException(desc);
|
|
}
|
|
merged = {};
|
|
util.shallowCopy(current, merged);
|
|
util.shallowCopy(desc, merged);
|
|
// merged should now have no missing fields
|
|
if (! ('value' in desc)) {
|
|
// desc is a GenericDescriptor. Nothing further to do.
|
|
stm.transactionFrame.recordDefine(self, name, merged);
|
|
return self.proxied;
|
|
}
|
|
// From here on, desc must be DataDescriptor
|
|
if ('value' in current) {
|
|
// current is DataDescriptor too
|
|
if ((! current.configurable) &&
|
|
(! current.writable) &&
|
|
(desc.writable || desc.value !== current.value)) {
|
|
throw new InvalidDescriptorException(desc);
|
|
}
|
|
stm.transactionFrame.recordDefine(self, name, merged);
|
|
return self.proxied;
|
|
} else {
|
|
// Current is Accessor; but we're converting to DataDescriptor
|
|
if (current.configurable) {
|
|
desc.writable = desc.writable || false;
|
|
stm.transactionFrame.recordDefine(self, name, merged);
|
|
return self.proxied;
|
|
} else {
|
|
throw new InvalidDescriptorException(desc);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
throw new WriteOutsideTransactionException();
|
|
}
|
|
},
|
|
|
|
erase: function (name) {
|
|
self.log("delete:", name);
|
|
var desc;
|
|
if ("_map" === name) {
|
|
return delete self.raw[name];
|
|
} else if (stm.inTransaction()) {
|
|
desc = handler.getOwnPropertyDescriptor(name);
|
|
if (undefined === desc) {
|
|
return true;
|
|
} else if (desc.configurable) {
|
|
// Just like in set: we don't do the delete here
|
|
stm.transactionFrame.recordDelete(self, name);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
throw new DeleteOutsideTransactionException();
|
|
}
|
|
},
|
|
|
|
fix: function () {
|
|
// TODO - make transaction aware. Somehow. Might not be possible...
|
|
self.log("*** fix ***");
|
|
if (Object.isFrozen(self.raw)) {
|
|
var result = {};
|
|
util.getOwnPropertyNames(self.raw).forEach(function (name) {
|
|
result[name] = util.getOwnPropertyDescriptor(self.raw, name);
|
|
});
|
|
return result;
|
|
}
|
|
// As long as obj is not frozen, the proxy won't allow
|
|
// itself to be fixed
|
|
return undefined; // will cause a TypeError to be thrown
|
|
},
|
|
|
|
has: function (name) {
|
|
self.log("has:", name);
|
|
var desc;
|
|
if ("_map" !== name) {
|
|
stm.recordRead(self);
|
|
}
|
|
if ("_map" === name || ! stm.inTransaction()) {
|
|
return !!handler.getPropertyDescriptor(name);
|
|
} else if (stm.transactionFrame.isDeleted(self, name)) {
|
|
return false;
|
|
} else {
|
|
desc = stm.transactionFrame.get(self, name);
|
|
if (undefined === desc) {
|
|
return !!handler.getPropertyDescriptor(name);
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
},
|
|
|
|
hasOwn: function (name) {
|
|
self.log("hasOwn:", name);
|
|
var desc;
|
|
if ("_map" !== name) {
|
|
stm.recordRead(self);
|
|
}
|
|
if ("_map" === name || ! stm.inTransaction()) {
|
|
return !!handler.getOwnPropertyDescriptor(name);
|
|
} else if (stm.transactionFrame.isDeleted(self, name)) {
|
|
return false;
|
|
} else {
|
|
desc = stm.transactionFrame.get(self, name);
|
|
if (undefined === desc) {
|
|
return !!handler.getOwnPropertyDescriptor(name);
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
},
|
|
|
|
get: function (receiver, name) {
|
|
self.log("get:", name);
|
|
var desc;
|
|
if ("_map" !== name) {
|
|
stm.recordRead(self);
|
|
}
|
|
if ("_map" === name || ! stm.inTransaction()) {
|
|
return self.raw[name];
|
|
} else if (stm.transactionFrame.isDeleted(self, name)) {
|
|
self.log("...has been deleted");
|
|
return undefined;
|
|
} else {
|
|
desc = stm.transactionFrame.get(self, name);
|
|
if (undefined === desc) {
|
|
desc = handler.getPropertyDescriptor(name);
|
|
if (undefined === desc) {
|
|
self.log("...not found");
|
|
return undefined;
|
|
} else if ('value' in desc) {
|
|
self.log("...found.");
|
|
return desc.value;
|
|
} else if ('get' in desc && undefined !== desc.get) {
|
|
return desc.get.call(self.proxied);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
} else {
|
|
self.log("...found in txn log");
|
|
return desc.value;
|
|
}
|
|
}
|
|
},
|
|
|
|
set: function (receiver, name, val) {
|
|
var desc, setter;
|
|
self.log("set:", name);
|
|
if ("_map" === name) {
|
|
self.raw[name] = val;
|
|
return true;
|
|
}
|
|
if (stm.inTransaction()) {
|
|
if (undefined === val ||
|
|
util.isPrimitive(val) ||
|
|
stm.isProxied(val)) {
|
|
// Note at no point do we do the real write here
|
|
desc = handler.getOwnPropertyDescriptor(name);
|
|
if (desc) {
|
|
if ('writable' in desc) {
|
|
if (desc.writable) {
|
|
stm.transactionFrame.recordWrite(self, name, val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else { // accessor
|
|
setter = desc.set;
|
|
if (setter) {
|
|
// we assume the setter is
|
|
// set up for use with atomize
|
|
setter.call(receiver, val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
// ok, we don't have it on us, but what about prototypes?
|
|
desc = handler.getPropertyDescriptor(name);
|
|
if (desc) {
|
|
if ('writable' in desc) {
|
|
if (desc.writable) { // fall through
|
|
} else {
|
|
return false;
|
|
}
|
|
} else { // accessor
|
|
setter = desc.set;
|
|
if (setter) {
|
|
// we assume the setter is
|
|
// set up for use with atomize
|
|
setter.call(receiver, val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (!Object.isExtensible(receiver)) {
|
|
return false;
|
|
} else {
|
|
stm.transactionFrame.recordWrite(self, name, val);
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
// it's not a tvar, explode
|
|
throw new NotATVarException();
|
|
}
|
|
} else {
|
|
throw new WriteOutsideTransactionException();
|
|
}
|
|
}, // bad behavior when set fails in non-strict mode
|
|
|
|
enumerate: function () {
|
|
self.log("enumerate");
|
|
var result = [], keys, i, name, desc;
|
|
stm.recordRead(self);
|
|
keys = handler.getPropertyNames();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
name = keys[i];
|
|
desc = handler.getPropertyDescriptor(name);
|
|
if (undefined !== desc && desc.enumerable) {
|
|
result.push(name);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
keys: function () {
|
|
self.log("keys");
|
|
var result = [], keys, i, name, desc;
|
|
stm.recordRead(self);
|
|
keys = handler.getOwnPropertyNames();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
name = keys[i];
|
|
desc = handler.getOwnPropertyDescriptor(name);
|
|
if (undefined !== desc && desc.enumerable) {
|
|
result.push(name);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// disgusting hack to get around fact IE won't parse
|
|
// JS if it sees 'delete' as a field.
|
|
handler['delete'] = handler['erase'];
|
|
|
|
return handler;
|
|
}
|
|
};
|
|
|
|
|
|
function Transaction(stm, id, parent, funs, cont, abort) {
|
|
this.stm = stm;
|
|
this.id = id;
|
|
this.funs = funs;
|
|
this.funIndex = 0;
|
|
if (undefined !== cont && undefined !== cont.call) {
|
|
this.cont = cont;
|
|
}
|
|
if (undefined !== abort && undefined !== abort.call) {
|
|
this.abort = abort;
|
|
}
|
|
if (undefined !== parent) {
|
|
this.parent = parent;
|
|
}
|
|
|
|
this.read = {};
|
|
this.created = {};
|
|
this.written = {};
|
|
|
|
this.readStack = [];
|
|
|
|
this.suspended = true;
|
|
}
|
|
|
|
Transaction.prototype = {
|
|
retryException: {
|
|
toString: function () { return "Internal Retry Exception"; }
|
|
},
|
|
deleted: {
|
|
toString: function () { return "Deleted Object"; }
|
|
},
|
|
|
|
log: function () {
|
|
var args;
|
|
if (this.stm.isLogging) {
|
|
args = Array.prototype.slice.call(arguments, 0);
|
|
args.unshift("[Txn " + this.id + "]");
|
|
this.stm.log.apply(this.stm, args);
|
|
}
|
|
},
|
|
|
|
reset: function (createds) {
|
|
if (0 === this.funIndex) {
|
|
this.readStack = [];
|
|
} else if (util.keys(this.read).length !== 0) {
|
|
this.readStack.push(this.read);
|
|
}
|
|
this.read = {};
|
|
this.written = {};
|
|
if (createds) {
|
|
this.created = {};
|
|
}
|
|
},
|
|
|
|
recordRead: function (parent) {
|
|
this.read[parent.id] = parent.version;
|
|
if (parent.unpopulated) {
|
|
throw new UnpopulatedException(parent);
|
|
}
|
|
},
|
|
|
|
recordCreation: function (value, meta) {
|
|
this.created[value.id] = {value: value,
|
|
meta: meta};
|
|
},
|
|
|
|
recordDelete: function (parent, name) {
|
|
this.recordDefine(parent, name, this.deleted);
|
|
},
|
|
|
|
recordWrite: function (parent, name, value) {
|
|
var desc = Object.getOwnPropertyDescriptor(parent.proxied, name);
|
|
if (undefined === desc) {
|
|
desc = {value : value,
|
|
enumerable : true,
|
|
configurable : true,
|
|
writable : true};
|
|
} else {
|
|
desc.value = value;
|
|
}
|
|
|
|
this.recordDefine(parent, name, desc);
|
|
},
|
|
|
|
recordDefine: function (parent, name, descriptor) {
|
|
if (this.deleted !== descriptor) {
|
|
if (util.hasOwnProp.call(descriptor, 'get') ||
|
|
util.hasOwnProp.call(descriptor, 'set')) {
|
|
throw new AccessorDescriptorsNotSupportedException(descriptor);
|
|
}
|
|
}
|
|
if (! util.hasOwnProp.call(this.written, parent.id)) {
|
|
this.written[parent.id] = {tvar : parent,
|
|
children : {}};
|
|
}
|
|
// this could get messy - name could be 'constructor', for
|
|
// example.
|
|
this.written[parent.id].children[name] = descriptor;
|
|
},
|
|
|
|
get: function (parent, name) {
|
|
if (util.hasOwnProp.call(this.written, parent.id) &&
|
|
util.hasOwnProp.call(this.written[parent.id].children, name)) {
|
|
if (this.deleted === this.written[parent.id].children[name]) {
|
|
return undefined;
|
|
} else {
|
|
return this.written[parent.id].children[name];
|
|
}
|
|
}
|
|
if (util.hasOwnProp.call(this, 'parent')) {
|
|
return this.parent.get(parent, name);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
isDeleted: function (parent, name) {
|
|
if (util.hasOwnProp.call(this.written, parent.id) &&
|
|
util.hasOwnProp.call(this.written[parent.id].children, name)) {
|
|
return this.deleted === this.written[parent.id].children[name];
|
|
}
|
|
if (util.hasOwnProp.call(this, 'parent')) {
|
|
return this.parent.isDeleted(parent, name);
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
keys: function (parent, predicate) {
|
|
var result, worklist, seen, obj, vars, keys, i;
|
|
result = [];
|
|
worklist = [];
|
|
seen = {};
|
|
|
|
if (undefined === predicate) {
|
|
predicate = function (obj, key) {
|
|
return obj[key].enumerable;
|
|
};
|
|
}
|
|
|
|
obj = this;
|
|
while (undefined !== obj) {
|
|
if (util.hasOwnProp.call(obj.written, parent.id)) {
|
|
worklist.push(obj.written[parent.id].children);
|
|
}
|
|
obj = obj.parent;
|
|
}
|
|
// use shift not pop to ensure we start at the child
|
|
// txn. Thus child txn 'delete' prevents parent
|
|
// 'write' of same var from showing up, as 'seen' will
|
|
// record the former and filter out the latter.
|
|
while (0 < worklist.length) {
|
|
vars = worklist.shift();
|
|
keys = util.keys(vars);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
if (! util.hasOwnProp.call(seen, keys[i])) {
|
|
seen[keys[i]] = true;
|
|
if ((this.deleted !== vars[keys[i]]) && predicate(vars, keys[i])) {
|
|
result.push(keys[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
getOwnPropertyNames: function (parent) {
|
|
return this.keys(parent, function (obj, key) { return true; });
|
|
},
|
|
|
|
run: function () {
|
|
if (util.hasOwnProp.call(this.stm, 'transactionFrame') &&
|
|
this.parent !== this.stm.transactionFrame &&
|
|
this !== this.stm.transactionFrame) {
|
|
throw new InternalException();
|
|
}
|
|
this.suspended = false;
|
|
this.funIndex = 0;
|
|
this.stm.transactionFrame = this;
|
|
while (! this.suspended) {
|
|
try {
|
|
return this.commit(this.funs[this.funIndex]());
|
|
} catch (err) {
|
|
if ((! util.isPrimitive(err)) &&
|
|
(UnpopulatedException.prototype ===
|
|
Object.getPrototypeOf(err))) {
|
|
// if we're in an orElse, we need to
|
|
// pretend that we hit a retry in every
|
|
// branch, so that we actually do the
|
|
// server-side retry
|
|
this.funIndex = 0;
|
|
err = this.retryException;
|
|
}
|
|
if (err === this.retryException) {
|
|
this.maybeSendRetry();
|
|
} else {
|
|
if (util.hasOwnProp.call(this, 'parent')) {
|
|
this.stm.transactionFrame = this.parent;
|
|
} else {
|
|
delete this.stm.transactionFrame;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
bumpCreated: function () {
|
|
var keys = util.keys(this.created).sort(),
|
|
i, obj;
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
obj = this.created[keys[i]].value;
|
|
if (obj.version === 0) {
|
|
obj.version = 1;
|
|
}
|
|
}
|
|
},
|
|
|
|
copyToParent: function (written) {
|
|
var worklist, obj, keys, i, key;
|
|
worklist = [this.read].concat(this.readStack);
|
|
while (worklist.length !== 0) {
|
|
obj = worklist.shift();
|
|
keys = util.keys(obj);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
key = keys[i];
|
|
this.parent.read[key] = obj[key];
|
|
}
|
|
}
|
|
|
|
keys = util.keys(this.created);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
key = keys[i];
|
|
this.parent.created[key] = this.created[key];
|
|
}
|
|
|
|
if (written) {
|
|
keys = util.keys(this.written);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
key = keys[i];
|
|
if (util.hasOwnProp.call(this.parent.written, key)) {
|
|
// parent has already written to some
|
|
// fields within key, so we need to merge
|
|
// our changes in (last writer wins, so
|
|
// this is quite simple):
|
|
util.shallowCopy(this.written[key].children,
|
|
this.parent.written[key].children);
|
|
} else {
|
|
this.parent.written[key] = this.written[key];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
commit: function (result) {
|
|
var success, failure, txnLog, read, written, created, self;
|
|
|
|
this.suspended = true;
|
|
|
|
if (util.hasOwnProp.call(this, 'parent')) {
|
|
// TODO - we could do a validation here - not a
|
|
// full commit. Would require server support.
|
|
|
|
this.copyToParent(true);
|
|
this.stm.transactionFrame = this.parent;
|
|
|
|
if (util.hasOwnProp.call(this, 'cont')) {
|
|
return this.cont(result);
|
|
} else {
|
|
return result;
|
|
}
|
|
|
|
} else {
|
|
delete this.stm.transactionFrame;
|
|
|
|
read = util.keys(this.read).length !== 0;
|
|
written = util.keys(this.written).length !== 0;
|
|
created = util.keys(this.created).length !== 0;
|
|
|
|
if (written || created) {
|
|
// if we wrote or created something then we
|
|
// have to go to the server, but the server
|
|
// can only fail us if we also read.
|
|
txnLog = this.cerealise();
|
|
txnLog.type = "commit";
|
|
this.bumpCreated();
|
|
if (read) {
|
|
self = this;
|
|
success = function () {
|
|
self.applyWritten();
|
|
return self.committed(result);
|
|
};
|
|
return this.stm.server.commit(
|
|
txnLog, success, this.failed.bind(this), this.abort);
|
|
} else {
|
|
success = function () {};
|
|
failure = function () {
|
|
throw new InternalException();
|
|
}
|
|
this.stm.server.commit(
|
|
txnLog, success, failure, this.abort);
|
|
// server can't fail us, so don't wait for it
|
|
this.applyWritten();
|
|
return this.committed(result);
|
|
}
|
|
} else {
|
|
// we didn't write or create. We don't need to
|
|
// go to the server at all.
|
|
return this.committed(result);
|
|
}
|
|
}
|
|
},
|
|
|
|
committed: function (result) {
|
|
if (util.hasOwnProp.call(this, 'cont')) {
|
|
return this.cont(result);
|
|
} else {
|
|
return result;
|
|
}
|
|
},
|
|
|
|
failed: function () {
|
|
// Created vars will be grabbed even on a failed
|
|
// commit. Thus do a full reset here.
|
|
this.reset(true);
|
|
return this.run();
|
|
},
|
|
|
|
applyWritten: function () {
|
|
var ids, i,j, parent, tvar, names, name, value;
|
|
ids = util.keys(this.written).sort();
|
|
for (i = 0; i < ids.length; i += 1) {
|
|
parent = this.written[ids[i]];
|
|
tvar = parent.tvar;
|
|
tvar.version += 1;
|
|
this.log("incr tvar", tvar.id, "to version", tvar.version);
|
|
names = util.keys(parent.children);
|
|
for (j = 0; j < names.length; j += 1) {
|
|
name = names[j];
|
|
value = parent.children[name];
|
|
if (this.deleted === value) {
|
|
this.log("Committing delete to", ids[i], ".", name);
|
|
delete tvar.raw[name];
|
|
} else {
|
|
this.log("Committing write to", ids[i], ".", name);
|
|
// mess for dealing with arrays, defineProperty, and some proxy mess
|
|
if (tvar.isArray && 'length' === name) {
|
|
tvar.raw[name] = value.value;
|
|
} else {
|
|
util.defineProperty(tvar.raw, name, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
retry: function () {
|
|
this.funIndex = (this.funIndex + 1) % this.funs.length;
|
|
this.suspended = this.funIndex === 0;
|
|
throw this.retryException;
|
|
},
|
|
|
|
maybeSendRetry: function () {
|
|
var self, restart, txnLog;
|
|
|
|
if (0 === this.funIndex) {
|
|
// If 0 === this.funIndex then we have done a full
|
|
// retry and will soon be waiting on the
|
|
// server. Thus we should continue unwinding the
|
|
// stack and thus rethrow if we have a parent. If
|
|
// we don't have a parent then we should absorb
|
|
// the exception and actually issue the retry to
|
|
// the server.
|
|
|
|
this.suspended = true;
|
|
|
|
if (util.hasOwnProp.call(this, 'parent')) {
|
|
this.copyToParent(false);
|
|
// We want to do a full retry, so we need to
|
|
// make sure our parent wants to do a full
|
|
// retry too.
|
|
this.parent.funIndex = 0;
|
|
this.stm.transactionFrame = this.parent;
|
|
throw this.retryException;
|
|
|
|
} else {
|
|
self = this;
|
|
txnLog = this.cerealise({created: true, read: true});
|
|
delete this.stm.transactionFrame;
|
|
|
|
// All created vars are about to become
|
|
// public. Thus bump vsn to 1.
|
|
this.bumpCreated();
|
|
|
|
restart = function () {
|
|
// Created vars will be grabbed even on a
|
|
// retry. Thus even if we have to restart
|
|
// here, we don't have to worry about the
|
|
// current createds any more.
|
|
self.reset(true);
|
|
return self.run();
|
|
};
|
|
|
|
txnLog.type = "retry";
|
|
|
|
this.stm.server.retry(txnLog, restart, this.abort);
|
|
}
|
|
} else {
|
|
// If 0 !== this.funIndex then we're in an orElse
|
|
// and we've hit a retry which we're going to
|
|
// service by changing to the next alternative and
|
|
// going round the run loop again. Thus absorb the
|
|
// exception, and don't exit the run loop. Do a
|
|
// partial reset - throw out the writes but keep
|
|
// the reads that led us here (and keep the
|
|
// creates; they won't be grabbed by the server
|
|
// until we talk to the server).
|
|
this.reset(false);
|
|
}
|
|
},
|
|
|
|
cerealise: function (obj) {
|
|
var worklist, seen, keys, i, self, key, meta, parent, names, j, value, desc;
|
|
self = this;
|
|
|
|
if (undefined === obj) {
|
|
obj = {read: {},
|
|
created: {},
|
|
written: {},
|
|
txnId: this.id};
|
|
} else {
|
|
obj.txnId = this.id;
|
|
}
|
|
|
|
if (util.hasOwnProp.call(obj, 'created') && obj.created) {
|
|
obj.created = {};
|
|
keys = util.keys(this.created).sort();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
key = keys[i];
|
|
obj.created[key] = {value: this.created[key].value.raw,
|
|
isArray: this.created[key].value.isArray,
|
|
version: this.created[key].value.version};
|
|
meta = this.created[key].meta;
|
|
if (undefined !== meta) {
|
|
obj.created[key].meta = meta;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (util.hasOwnProp.call(obj, 'read') && obj.read) {
|
|
obj.read = {};
|
|
seen = {};
|
|
worklist = [this.read].concat(this.readStack);
|
|
while (worklist.length !== 0) {
|
|
value = worklist.shift();
|
|
keys = util.keys(value).sort();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
key = keys[i];
|
|
if (! util.hasOwnProp.call(seen, key)) {
|
|
seen[key] = true;
|
|
if (util.hasOwnProp.call(obj, 'created') ||
|
|
! util.hasOwnProp.call(this.created, key)) {
|
|
obj.read[keys[i]] = {version: value[key]};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (util.hasOwnProp.call(obj, 'written') && obj.written) {
|
|
obj.written = {};
|
|
keys = util.keys(this.written).sort();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
parent = {};
|
|
obj.written[keys[i]] = parent;
|
|
names = util.keys(this.written[keys[i]].children);
|
|
for (j = 0; j < names.length; j += 1) {
|
|
value = this.written[keys[i]].children[names[j]];
|
|
if (this.deleted === value) {
|
|
parent[names[j]] = {deleted: true};
|
|
} else if (util.isPrimitive(value.value)) {
|
|
parent[names[j]] = value;
|
|
} else {
|
|
desc = {};
|
|
util.shallowCopy(value, desc);
|
|
delete desc.value;
|
|
desc.tvar = this.stm.asTVar(value.value).id;
|
|
parent[names[j]] = desc;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
};
|
|
|
|
|
|
STM = function () {
|
|
this.tVarCount = 0;
|
|
this.txnCount = 0;
|
|
|
|
this.objToTVar = new util.Map();
|
|
this.proxiedToTVar = new util.Map();
|
|
this.idToTVar = {};
|
|
this.retryException = {};
|
|
this.server = this.noopServer();
|
|
this.root();
|
|
this.logPrefix = "";
|
|
};
|
|
|
|
STM.prototype = {
|
|
isLogging: false,
|
|
|
|
logging: function (bool) {
|
|
this.isLogging = bool;
|
|
},
|
|
|
|
setLogPrefix: function (prefix) {
|
|
this.logPrefix = prefix;
|
|
},
|
|
|
|
log: function () {
|
|
var args;
|
|
if (this.isLogging) {
|
|
args = Array.prototype.slice.call(arguments, 0);
|
|
if (0 !== this.logPrefix.length) {
|
|
args.unshift(this.logPrefix);
|
|
console.log.apply(console, args);
|
|
} else {
|
|
console.log.apply(console, args);
|
|
}
|
|
}
|
|
},
|
|
|
|
noopServer: function () {
|
|
var self = this;
|
|
return {
|
|
commit: function (txnLog, success, failure) {
|
|
self.log("Committing txn log:", txnLog);
|
|
return success();
|
|
},
|
|
|
|
retry: function (txnLog, restart) {
|
|
// default implementation is just going to spin on
|
|
// this for the time being.
|
|
self.log("Retry with txn log:", txnLog);
|
|
return restart();
|
|
}
|
|
};
|
|
},
|
|
|
|
inTransaction: function () {
|
|
return util.hasOwnProp.call(this, 'transactionFrame');
|
|
},
|
|
|
|
orElse: function (funs, cont, abort) {
|
|
var parent = this.transactionFrame,
|
|
txn;
|
|
this.txnCount += 1;
|
|
txn = new Transaction(this, this.txnCount, parent, funs, cont, abort);
|
|
return txn.run();
|
|
},
|
|
|
|
atomically: function (fun, cont, abort) {
|
|
return this.orElse([fun], cont, abort);
|
|
},
|
|
|
|
retry: function () {
|
|
if (!this.inTransaction()) {
|
|
throw new RetryOutsideTransactionException();
|
|
}
|
|
this.transactionFrame.retry();
|
|
},
|
|
|
|
recordRead: function (parent) {
|
|
if (this.inTransaction()) {
|
|
this.transactionFrame.recordRead(parent);
|
|
}
|
|
},
|
|
|
|
recordCreation: function (value, meta) {
|
|
if (this.inTransaction()) {
|
|
this.transactionFrame.recordCreation(value, meta);
|
|
} else {
|
|
var self = this;
|
|
self.atomically(function () {
|
|
self.transactionFrame.recordCreation(value, meta);
|
|
});
|
|
}
|
|
},
|
|
|
|
isProxied: function (obj) {
|
|
return this.proxiedToTVar.has(obj);
|
|
},
|
|
|
|
asTVar: function (proxied) {
|
|
return this.proxiedToTVar.get(proxied);
|
|
},
|
|
|
|
// always returns a TVar; not a proxied obj
|
|
ensureTVar: function (obj, meta) {
|
|
var val, parentId;
|
|
if (undefined === obj || util.isPrimitive(obj)) {
|
|
return {proxied: obj, raw: obj};
|
|
}
|
|
val = this.proxiedToTVar.get(obj);
|
|
if (undefined === val) {
|
|
val = this.objToTVar.get(obj);
|
|
if (undefined === val) {
|
|
this.tVarCount += 1;
|
|
val = new TVar(this.tVarCount, obj, this);
|
|
this.proxiedToTVar.set(val.proxied, val);
|
|
this.objToTVar.set(obj, val);
|
|
this.idToTVar[val.id] = val;
|
|
this.recordCreation(val, meta);
|
|
return val;
|
|
} else {
|
|
// found it in the cache
|
|
return val;
|
|
}
|
|
} else {
|
|
// obj was already proxied
|
|
return val;
|
|
}
|
|
},
|
|
|
|
lift: function (obj, meta) {
|
|
return this.ensureTVar(obj, meta).proxied;
|
|
},
|
|
|
|
watch: function () {
|
|
var self = this,
|
|
objs = Array.prototype.slice.call(arguments, 0),
|
|
cont = objs.shift(),
|
|
fun, i;
|
|
for (i = 0; i < objs.length; i += 1) {
|
|
objs[i] = self.lift(objs[i]);
|
|
}
|
|
fun = function (copies) {
|
|
var i, j, obj, keys, seen, delta, prev, field, deltas, copies2, retry;
|
|
self.atomically(
|
|
function () {
|
|
deltas = new util.Map();
|
|
copies2 = new util.Map();
|
|
retry = true;
|
|
for (i = 0; i < objs.length; i += 1) {
|
|
obj = objs[i];
|
|
copies2.set(obj, TVar.prototype.clone(obj));
|
|
delta = { added: [],
|
|
modified: [],
|
|
deleted: [] };
|
|
if (undefined === copies) {
|
|
delta.added = Object.keys(obj);
|
|
// first time through, existence
|
|
// of empty obj should be enough
|
|
// to trigger
|
|
retry = false;
|
|
deltas.set(obj, delta);
|
|
} else {
|
|
prev = copies.get(obj);
|
|
seen = {};
|
|
keys = Object.keys(obj).concat(Object.keys(prev));
|
|
for (j = 0; j < keys.length; j += 1) {
|
|
field = keys[j];
|
|
if (util.hasOwnProp.call(seen, field)) {
|
|
continue;
|
|
}
|
|
seen[field] = true;
|
|
if (util.hasOwnProp.call(obj, field) &&
|
|
util.hasOwnProp.call(prev, field)) {
|
|
if (obj[field] !== prev[field]) {
|
|
delta.modified.push(field);
|
|
}
|
|
} else if (util.hasOwnProp.call(obj, keys[j])) {
|
|
delta.added.push(field);
|
|
} else {
|
|
delta.deleted.push(field);
|
|
}
|
|
}
|
|
if ((delta.added.length + delta.modified.length +
|
|
delta.deleted.length) > 0) {
|
|
deltas.set(obj, delta);
|
|
retry = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (retry) {
|
|
self.retry();
|
|
} else {
|
|
return {copies: copies2, result: cont(true, deltas)};
|
|
}
|
|
}, function (result) {
|
|
if (cont(false, result.result)) {
|
|
fun(result.copies);
|
|
}
|
|
});
|
|
};
|
|
fun(undefined);
|
|
},
|
|
|
|
access: function (obj, field) {
|
|
var tvar, result;
|
|
tvar = this.asTVar(obj);
|
|
if (undefined === tvar) {
|
|
result = obj[field];
|
|
if (typeof result === 'function') {
|
|
return result.bind(obj);
|
|
} else {
|
|
return result;
|
|
}
|
|
} else {
|
|
return tvar.handler.get(obj, field);
|
|
}
|
|
},
|
|
|
|
assign: function (obj, field, value) {
|
|
var tvar = this.asTVar(obj);
|
|
if (undefined === tvar) {
|
|
return obj[field] = value;
|
|
} else {
|
|
return tvar.handler.set(tvar.proxied, field, value);
|
|
}
|
|
},
|
|
|
|
enumerate: function (obj) {
|
|
var tvar = this.asTVar(obj);
|
|
if (undefined === tvar) {
|
|
return obj;
|
|
} else {
|
|
return tvar.handler.enumerate();
|
|
}
|
|
},
|
|
|
|
has: function (obj, field) {
|
|
var tvar = this.asTVar(obj);
|
|
if (undefined === tvar) {
|
|
return field in obj;
|
|
} else {
|
|
return tvar.handler.has(field);
|
|
}
|
|
},
|
|
|
|
erase: function (obj, field) {
|
|
var tvar = this.asTVar(obj);
|
|
if (undefined === tvar) {
|
|
return delete obj[field];
|
|
} else {
|
|
// IE can't cope if you write ...handler.delete(...
|
|
return tvar.handler['delete'](field);
|
|
}
|
|
},
|
|
|
|
root: function () {
|
|
var obj, val;
|
|
if (0 === this.tVarCount) {
|
|
obj = {};
|
|
this.tVarCount += 1;
|
|
val = new TVar(this.tVarCount, obj, this);
|
|
this.proxiedToTVar.set(val.proxied, val);
|
|
this.objToTVar.set(obj, val);
|
|
this.idToTVar[val.id] = val;
|
|
val.version = 0;
|
|
val.unpopulated = true;
|
|
return val.proxied;
|
|
} else {
|
|
return this.idToTVar[1].proxied;
|
|
}
|
|
},
|
|
|
|
applyUpdates: function (updates) {
|
|
var keys, names, update, tvar, i, j, value, obj;
|
|
keys = util.keys(updates);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
update = updates[keys[i]];
|
|
tvar = this.idToTVar[keys[i]];
|
|
if (undefined === tvar) {
|
|
if (update.isArray) {
|
|
obj = [];
|
|
} else {
|
|
obj = {};
|
|
}
|
|
tvar = new TVar(keys[i], obj, this);
|
|
this.proxiedToTVar.set(tvar.proxied, tvar);
|
|
this.objToTVar.set(obj, tvar);
|
|
this.idToTVar[tvar.id] = tvar;
|
|
}
|
|
|
|
// It's possible to see an update for a var with
|
|
// the same version as we already have due to
|
|
// multiple txns being rejected for the same
|
|
// reason (or a retry and commit from the same
|
|
// client - the commit may alter the var the retry
|
|
// is watching and thus prompt an update after the
|
|
// commit completes). If the versions are equal,
|
|
// then just ignore it.
|
|
|
|
if (tvar.version > update.version) {
|
|
console.error("Invalid update detected: " + tvar.id +
|
|
" local vsn: " + tvar.version + "; remote vsn: " + update.version);
|
|
}
|
|
tvar.unpopulated = 0 === update.version;
|
|
if (tvar.version === update.version) {
|
|
continue;
|
|
}
|
|
|
|
tvar.version = update.version;
|
|
|
|
names = util.keys(tvar.raw);
|
|
for (j = 0; j < names.length; j += 1) {
|
|
if ("_map" !== names[j] && ! util.hasOwnProp.call(update.value, names[j])) {
|
|
delete tvar.raw[names[j]];
|
|
}
|
|
}
|
|
|
|
names = util.keys(update.value);
|
|
for (j = 0; j < names.length; j += 1) {
|
|
this.applyUpdate(tvar, names[j], update.value[names[j]], updates);
|
|
}
|
|
}
|
|
},
|
|
|
|
applyUpdate: function (tvar, name, desc, updates) {
|
|
var obj, tvar2, desc2 = {};
|
|
if (! util.hasOwnProp.call(desc, 'tvar')) {
|
|
if (tvar.isArray && 'length' === name) {
|
|
tvar.raw[name] = desc.value;
|
|
} else {
|
|
util.defineProperty(tvar.raw, name, desc);
|
|
}
|
|
} else {
|
|
if (undefined === this.idToTVar[desc.tvar]) {
|
|
if (util.hasOwnProp.call(updates, desc.tvar)) {
|
|
// we're going to create a new empty var
|
|
// so that we can proceed here.
|
|
if (updates[desc.tvar].isArray) {
|
|
obj = [];
|
|
} else {
|
|
obj = {};
|
|
}
|
|
tvar2 = new TVar(desc.tvar, obj, this);
|
|
this.proxiedToTVar.set(tvar2.proxied, tvar2);
|
|
this.objToTVar.set(obj, tvar2);
|
|
this.idToTVar[tvar2.id] = tvar2;
|
|
} else {
|
|
// not known already, and not defined by updates!
|
|
throw new InternalException();
|
|
}
|
|
}
|
|
util.shallowCopy(desc, desc2);
|
|
desc2.value = this.idToTVar[desc.tvar].proxied;
|
|
delete desc2.tvar;
|
|
util.defineProperty(tvar.raw, name, desc2);
|
|
}
|
|
}
|
|
};
|
|
|
|
}());
|
|
|
|
(function () {
|
|
var compatNeeded = false,
|
|
publicMethods = ['logging', 'setLogPrefix', 'inTransaction', 'orElse',
|
|
'atomically', 'retry', 'lift', 'access', 'assign',
|
|
'enumerate', 'has', 'erase', 'watch'],
|
|
atomizeImpl;
|
|
|
|
Atomize = function () {
|
|
var args = Array.prototype.slice.call(arguments, 0),
|
|
stm = new STM();
|
|
util.lift(stm, this, publicMethods);
|
|
this.root = stm.root();
|
|
atomizeImpl(this, stm, args);
|
|
};
|
|
Atomize.prototype = {
|
|
onPreAuthenticated: function (message, sockjs) {
|
|
return true;
|
|
},
|
|
|
|
onAuthenticated: function () {
|
|
},
|
|
|
|
connect: function () {
|
|
},
|
|
|
|
close: function () {
|
|
}
|
|
};
|
|
|
|
if (typeof exports !== "undefined") {
|
|
// we're in node
|
|
|
|
if ("undefined" === typeof Proxy) {
|
|
compatNeeded = true;
|
|
}
|
|
|
|
atomizeImpl = function (external, stm, args) {
|
|
var ClientCtor = args[0],
|
|
serverEventEmitter = args[1],
|
|
inflight = {},
|
|
resultFuns = [],
|
|
FakeConn, client;
|
|
|
|
if (compatNeeded) {
|
|
require('./compat').compat.load(stm);
|
|
}
|
|
|
|
function runResultFuns () {
|
|
while (resultFuns.length > 0) {
|
|
(resultFuns.pop())();
|
|
}
|
|
}
|
|
|
|
FakeConn = function () {
|
|
this.id = "server";
|
|
this.emitter = new events.EventEmitter();
|
|
util.lift(this.emitter, this,
|
|
['on', 'once', 'removeListener', 'removeAllListeners', 'emit']);
|
|
};
|
|
FakeConn.prototype = {
|
|
write: function (msg) {
|
|
// this is the server writing a message back
|
|
// to the client.
|
|
|
|
var txnLog, txnId, txn, fun;
|
|
txnLog = Cereal.parse(msg);
|
|
switch (txnLog.type) {
|
|
case 'result':
|
|
txnId = txnLog.txnId;
|
|
txn = inflight[txnId];
|
|
delete inflight[txnId];
|
|
stm.log("[Txn " + txnId + "] response received:", txnLog.result);
|
|
fun = txn[txnLog.result];
|
|
if (undefined !== fun && undefined !== fun.call) {
|
|
if (txn.runImmediately) {
|
|
runResultFuns();
|
|
fun();
|
|
} else {
|
|
resultFuns.push(fun);
|
|
}
|
|
}
|
|
break;
|
|
case 'updates':
|
|
stm.log("Received Updates:", txnLog);
|
|
runResultFuns();
|
|
stm.applyUpdates(txnLog.updates);
|
|
break;
|
|
default:
|
|
stm.log("Confused");
|
|
}
|
|
}
|
|
};
|
|
|
|
client = new ClientCtor(new FakeConn(), serverEventEmitter);
|
|
|
|
stm.server = {
|
|
commit: function (txnLog, success, failure, abort) {
|
|
var serialised = Cereal.stringify(txnLog),
|
|
obj = {txnLog: serialised,
|
|
success: success,
|
|
failure: failure,
|
|
abort: abort};
|
|
inflight[txnLog.txnId] = obj;
|
|
stm.log("[Txn " + txnLog.txnId + "] sending commit:", txnLog);
|
|
client.dispatch(serialised);
|
|
// set this *after* we've potentially already run it...!
|
|
obj.runImmediately = true;
|
|
runResultFuns();
|
|
},
|
|
|
|
retry: function (txnLog, restart, abort) {
|
|
var serialised = Cereal.stringify(txnLog),
|
|
obj = {txnLog: txnLog,
|
|
restart: restart,
|
|
abort: abort};
|
|
inflight[txnLog.txnId] = obj;
|
|
stm.log("[Txn " + txnLog.txnId + "] sending retry:", txnLog);
|
|
client.dispatch(serialised);
|
|
// set this *after* we've potentially already run it...!
|
|
obj.runImmediately = true;
|
|
runResultFuns();
|
|
}
|
|
};
|
|
};
|
|
|
|
exports.Atomize = Atomize;
|
|
exports.Map = util.Map;
|
|
|
|
} else {
|
|
(function () {
|
|
if ("undefined" === typeof Proxy) {
|
|
var scripts = document.getElementsByTagName('script'),
|
|
index = scripts.length - 1,
|
|
myScript = scripts[index],
|
|
script = document.createElement('script');
|
|
|
|
script.src = myScript.src.substring(0, 1 + (myScript.src.lastIndexOf('/'))) + "compat.js";
|
|
|
|
document.getElementsByTagName('head')[0].appendChild(script);
|
|
|
|
compatNeeded = true;
|
|
}
|
|
}());
|
|
|
|
if (undefined === window.SockJS) {
|
|
console.warn("SockJS not found. Assuming offline-mode.");
|
|
|
|
atomizeImpl = function (external, stm) {
|
|
if (compatNeeded) {
|
|
AtomizeCompat.load(stm);
|
|
}
|
|
|
|
external.connect = function () {
|
|
external.onAuthenticated();
|
|
};
|
|
};
|
|
|
|
} else {
|
|
|
|
atomizeImpl = function (external, stm, args) {
|
|
var url = args[0],
|
|
inflight = {},
|
|
queue = [],
|
|
reconnector,
|
|
drainEnqueued,
|
|
sockjs;
|
|
|
|
if (compatNeeded) {
|
|
AtomizeCompat.load(stm);
|
|
}
|
|
|
|
if (undefined === url) {
|
|
if (location.protocol !== 'http:' &&
|
|
location.protocol !== 'https:') {
|
|
console.warn("No url provided. Assuming offline-mode.");
|
|
} else {
|
|
url = location.protocol + '//' + location.host + '/atomize';
|
|
}
|
|
}
|
|
|
|
if (undefined !== url) {
|
|
external.ready = false;
|
|
|
|
reconnector = {
|
|
initialDelay: 2000, // 2 seconds
|
|
maxDelay: 180000, // 3 minutes
|
|
|
|
connected: function () {
|
|
stm.log("Connected");
|
|
reconnector.reconnect = true;
|
|
reconnector.delay = reconnector.initialDelay;
|
|
},
|
|
|
|
connect: function () {
|
|
stm.log("Attempting connection");
|
|
external.connect();
|
|
},
|
|
|
|
disconnected: function () {
|
|
stm.log("Disconnected");
|
|
if (reconnector.reconnect) {
|
|
setTimeout(reconnector.connect, reconnector.delay);
|
|
reconnector.delay += Math.floor(Math.random() * reconnector.delay);
|
|
reconnector.delay = reconnector.delay > reconnector.maxDelay
|
|
? reconnector.maxDelay : reconnector.delay;
|
|
}
|
|
}
|
|
};
|
|
|
|
drainEnqueued = function () {
|
|
var i;
|
|
for (i = 0; i < queue.length; i += 1) {
|
|
sockjs.send(queue[i]);
|
|
}
|
|
queue = [];
|
|
};
|
|
|
|
external.close = function () {
|
|
reconnector.reconnect = false;
|
|
sockjs.close();
|
|
};
|
|
|
|
external.connect = function () {
|
|
reconnector.connected();
|
|
sockjs = new SockJS(url);
|
|
|
|
sockjs.onopen = function () {
|
|
stm.log("Connected to server", url,
|
|
"(using", sockjs.protocol, ")");
|
|
reconnector.connected();
|
|
external.ready = external.onPreAuthenticated(undefined, sockjs);
|
|
if (external.ready) {
|
|
drainEnqueued();
|
|
external.onAuthenticated();
|
|
} else {
|
|
return;
|
|
}
|
|
};
|
|
|
|
sockjs.onclose = function (e) {
|
|
var keys, i, obj;
|
|
stm.log("Disconnected from server", url,
|
|
"(", e.status, e.reason, ")");
|
|
external.ready = false;
|
|
keys = util.keys(inflight).sort();
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
queue.push(inflight[keys[i]].txnLog);
|
|
}
|
|
reconnector.disconnected();
|
|
};
|
|
|
|
sockjs.onmessage = function (e) {
|
|
var txnLog, txnId, txn, fun;
|
|
if (!external.ready) {
|
|
external.ready = external.onPreAuthenticated(e, sockjs);
|
|
if (external.ready) {
|
|
drainEnqueued();
|
|
external.onAuthenticated();
|
|
}
|
|
return; // authentication consumed e, so return here
|
|
}
|
|
txnLog = Cereal.parse(e.data);
|
|
switch (txnLog.type) {
|
|
case 'result':
|
|
txnId = txnLog.txnId;
|
|
txn = inflight[txnId];
|
|
delete inflight[txnId];
|
|
stm.log("[Txn " + txnId + "] response received:", txnLog.result);
|
|
fun = txn[txnLog.result];
|
|
if (undefined !== fun && undefined !== fun.call) {
|
|
fun();
|
|
}
|
|
break;
|
|
case 'updates':
|
|
stm.log("Received Updates:", txnLog);
|
|
stm.applyUpdates(txnLog.updates);
|
|
break;
|
|
default:
|
|
stm.log("Received unexpected message from server:",
|
|
JSON.stringify(txnLog));
|
|
throw new InternalException();
|
|
}
|
|
};
|
|
};
|
|
|
|
stm.server = {
|
|
commit: function (txnLog, success, failure, abort) {
|
|
var serialised = Cereal.stringify(txnLog),
|
|
obj = {txnLog: serialised,
|
|
success: success,
|
|
failure: failure,
|
|
abort: abort};
|
|
inflight[txnLog.txnId] = obj;
|
|
if (! external.ready) {
|
|
queue.push(serialised);
|
|
return;
|
|
}
|
|
stm.log("[Txn " + txnLog.txnId + "] sending commit:", txnLog);
|
|
sockjs.send(serialised);
|
|
},
|
|
|
|
retry: function (txnLog, restart, abort) {
|
|
var serialised = Cereal.stringify(txnLog),
|
|
obj = {txnLog: txnLog,
|
|
restart: restart,
|
|
abort: abort};
|
|
inflight[txnLog.txnId] = obj;
|
|
if (! external.ready) {
|
|
queue.push(serialised);
|
|
return;
|
|
}
|
|
stm.log("[Txn " + txnLog.txnId + "] sending retry:", txnLog);
|
|
sockjs.send(serialised);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
}());
|
|
|
|
}(this));
|