UNPKG

atomize-client

Version:

Client library for AtomizeJS: JavaScript DSTM

1,131 lines (1,027 loc) 79.2 kB
/*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;