control-freak-ide/server/nodejs/nxapp/xide/do/client/lib/atomizejs/atomize.js
plastic-hub-dev-node-saturn 538369cff7 latest
2021-05-12 18:35:18 +02:00

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));