shared-updated
Version:
Modern fork of shared (Kevin Jones), updated for latest Node.js and MongoDB
480 lines (417 loc) • 14.7 kB
text/typescript
// Copyright (c) Kevin Jones. All rights reserved. Licensed under the Apache
// License, Version 2.0. See LICENSE.txt in the project root for complete
// license information.
/// <reference path='import.ts' />
/// <reference path='tracker.ts' />
/// <reference path='mtx.ts' />
module shared {
export module mtx {
export class ObjectCache {
private _cache: utils.Map = new utils.Map(utils.hash); // Cached objects
public find(id: utils.uid): any {
return this._cache.find(id.toString());
}
public insert(id: utils.uid, value:any): bool {
return this._cache.insert(id.toString(),value);
}
public remove(id: utils.uid): bool {
return this._cache.remove(id.toString());
}
}
/*
* A mtx consist of three parts: a readset, a new set and a set of changes
* (cset). The readset contains obj->revision mappings which much be checked
* prior to detect conflict. There must be at least one entry in the readset
* and entries may not reference objects in the newset. The newset contains
* id->object mappings of new objects being introduced. The newset must be
* ordered to avoid non-existent references to other new objects.The change set
* contains a list of object change instructions, these may reference any
* objects in the newset.
*
* The in-memory & on-the-wire format of a mtx differ in how objects are
* referenced (by id or directly). This can help improve local mtx commit
* performance but can be confusing.
*
* See TrackCache for more details.
*/
export class mtxFactory implements tracker.TrackCache extends utils.UniqueObject {
public disable: number = 0;
private _mtx: MTX = new MTX();
private _collected = false;
constructor () {
super();
}
/*
* The current change set, only exposed to aid debugging.
*/
cset(): any [] {
return this._mtx.cset.array();
}
/*
* Record object as been read
*/
markRead(value: any) {
var t = tracker.getTracker(value);
utils.dassert(t.tc()===this);
this._mtx.rset.insert(t.id(), t.rev());
}
/*
* Readset access, only exposed to aid debugging
*/
readsetSize() : number {
return this._mtx.rset.size();
}
readsetObject(id: utils.uid) : number {
return this._mtx.rset.find(id.toString())
}
/*
* Newset access, only exposed to aid debugging
*/
newsetSize() : number {
return this._mtx.nset.size();
}
newsetObject(id: utils.uid) : any {
var ent=this._mtx.nset.first(function (entry: any) {
return entry.id.toString() === id.toString();
});
if (ent) return ent.obj;
return null;
}
/*
* Object change recording
*/
addNew(obj: any, prop: string, value: any, lasttx: number): number {
if (utils.isObjectOrArray(value))
this.valueId(value);
var v = value;
if (typeof v === 'function') v = null;
this._mtx.cset.push({obj:obj, write: prop, value: v, lasttx: lasttx});
return this._mtx.cset.size() - 1;
}
addWrite(obj: any, prop: string, value: any, last: any, lasttx: number): number {
if (utils.isObjectOrArray(value))
this.valueId(value);
var v = value;
if (typeof v === 'function') v = null;
this._mtx.cset.push({obj:obj, write: prop, value: v, last: last, lasttx: lasttx});
return this._mtx.cset.size() - 1;
}
addDelete(obj: any, prop: string, lasttx: number): number {
this._mtx.cset.push({obj:obj, del: prop, lasttx: lasttx});
return this._mtx.cset.size() - 1;
}
addReverse(obj: any, lasttx: number): number {
this._mtx.cset.push({obj:obj, reverse: true, lasttx: lasttx});
return this._mtx.cset.size() - 1;
}
addSort(obj: any, lasttx: number): number {
this._mtx.cset.push({ obj: obj, sort: true, lasttx: lasttx });
return this._mtx.cset.size() - 1;
}
addShift(obj: any, at: number, size: number, lasttx: number): number {
this._mtx.cset.push({ obj: obj, shift: at, size: size, lasttx: lasttx });
return this._mtx.cset.size() - 1;
}
addUnshift(obj: any, at: number, size: number, lasttx: number): number {
this._mtx.cset.push({ obj: obj, unshift: at, size: size, lasttx: lasttx });
return this._mtx.cset.size() - 1;
}
/*
* Utility to change a sort record with a reinit during post-processing
*/
replaceSort(at: number, obj: any, values: string) {
utils.dassert(this._mtx.cset.at(at).sort !== undefined);
// Null out history
var t = tracker.getTracker(obj);
var dead = t.lastChange();
while (dead !== -1) {
var c = dead;
dead = this._mtx.cset.at(dead).lasttx;
this._mtx.cset.setAt(c, null);
}
// Insert re-init
this._mtx.cset.push({ obj: obj, reinit: values, lasttx: -1 });
t.setLastChange(this._mtx.cset.size() - 1);
}
/*
* Return the mtx record of stored changes.
*/
mtx(cache: ObjectCache) : MTX {
// We must collect over readset to build complete picture, but only once
utils.dassert(this._collected === false);
this._collected = true;
this.collect(cache);
// Return the formed mtx
return this._mtx;
}
resetMtx(): void {
// Restart with a new mtx
this._mtx = new MTX();
this._collected = false;
}
okMtx(store: ObjectCache): void {
// Reset last change
this._mtx.cset.apply(function (ci: ChangeItem) {
if (ci !== null) {
var t = tracker.getTracker(ci.obj);
t.setLastChange(-1);
}
});
this.resetMtx();
}
undoMtx(cache: ObjectCache): void {
this.disable++;
// We must collect over readset to build complete picture
if (!this._collected)
this.collect(cache);
// Unwind the cset actions
var i = this._mtx.cset.size() - 1;
while (i >= 0) {
var e = this._mtx.cset.at(i);
if (e !== null) {
var t = tracker.getTracker(e.obj);
if (!t.isDead()) {
// Try reverse if can
if (e.write !== undefined) {
if (e.last !== undefined) {
e.obj[e.write] = e.last;
} else {
if (utils.isArray(e.obj))
e.obj.splice(parseInt(e.write), 1);
else
delete e.obj[e.write];
}
} else {
// Conservatively kill everything else
t.kill();
cache.remove(t.id());
}
}
}
i--;
}
// Reset internal state
var i = this._mtx.cset.size() - 1;
while (i >= 0) {
var e = this._mtx.cset.at(i);
if (e !== null) {
var t = tracker.getTracker(e.obj);
t.downrev(e.obj);
}
i--;
}
this.resetMtx();
this.disable--;
}
/*
* Obtain an id for any object. If passed an untracked object an id
* will be assigned to it although the object will not be tracked.
* Objects that are already been tracked by a different cache cause
* an exception.
*/
valueId(value: any): utils.uid {
utils.dassert(utils.isObjectOrArray(value));
var t = tracker.getTrackerUnsafe(value);
if (t === null) {
if (value._pid === undefined) {
// Recurse into props of new object
var keys = Object.keys(value);
for (var k = 0; k < keys.length; k++) {
var key = keys[k];
if (utils.isObjectOrArray(value[key])) {
this.valueId(value[key]);
}
}
// Record this as new object
Object.defineProperty(value, '_pid', {
value: utils.UID()
});
this._mtx.nset.push({ id: value._pid, obj: value });
}
return value._pid;
} else {
if (t.tc() !== this)
utils.defaultLogger().fatal('Objects can not be used in multiple stores');
return value._tracker.id();
}
}
/*
* Obtain version number for any objects. Untracked objects are assumed
* to be version zero.
*/
valueRev(value: any) : number {
if (value._tracker === undefined) {
return 0;
} else {
return value._tracker.rev();
}
}
/*
* Run post-processing over all object that have been read.
*/
private collect(cache: ObjectCache) : void {
// Collect over the readset
var that = this;
this._mtx.rset.apply(function (key, value) {
var obj = cache.find(key);
utils.dassert(obj != null);
that.collectObject(obj);
return true;
});
};
/*
* Run post-processing over a specific object, for debug proposes only
*/
collectObject(obj: any) {
utils.dassert(tracker.isTracked(obj));
if (obj instanceof Array) {
this.arrayChanges(obj);
} else {
this.objectChanges(obj);
}
}
/*
* Post-processing changes on an object
*/
private objectChanges(obj) : void {
var t = tracker.getTracker(obj);
t.tc().disable++;
// Loop old props to find any to delete
var oldProps = utils.cloneArray(t.type().props());
var newProps = Object.keys(obj);
for (var i = 0; i < oldProps.length; i++) {
if (!obj.hasOwnProperty(oldProps[i]) || !tracker.isPropTracked(obj, oldProps[i])) {
t.addDelete(obj, oldProps[i]);
} else {
// Remove any old ones for next step
var idx = newProps.indexOf(oldProps[i]);
newProps[idx] = null;
}
}
// Add any new props
for (var i = 0; i < newProps.length; i++) {
if (newProps[i] !== null) {
t.addNew(obj, newProps[i], obj[newProps[i]]);
t.track(obj, newProps[i]);
}
}
if (t.hasChanges())
t.uprev(obj);
t.tc().disable--;
}
/*
* Post-processing changes on an array
*/
private arrayChanges(obj) : void {
var t = tracker.getTracker(obj);
t.tc().disable++;
// Sorted arrays are treated as being fully re-initialized as we
// can't track the impact of the sort.
// First we construct an array of the writes upto the last sort
// if there was one
var at = t.lastChange();
var writeset = [];
while (at !== -1) {
if (this._mtx.cset.at(at).sort !== undefined) {
// Replace sort by a re-init
var v = serial.writeObject(t.tc(), obj, '');
this.replaceSort(at, obj, v);
t.uprev(obj);
t.tc().disable--;
return;
} else {
writeset.unshift(this._mtx.cset.at(at));
}
at = this._mtx.cset.at(at).lasttx;
}
// Next we adjust the original props to account for how the array
// has been shifted so we can detect new props and delete old ones
// correctly.
// REMEMBER the props maybe sparse but shift/unshift is abs
var oldProps = utils.cloneArray(t.type().props());
for (var i = 0; i < writeset.length; i++) {
if (writeset[i].shift != undefined) {
var at = writeset[i].shift;
var size = writeset[i].size;
var j = 0;
while (true) {
if (j === oldProps.length) break;
var idx = +oldProps[j];
if (idx >= at && idx < at + size) {
oldProps.splice(j, 1);
} else if (idx >= at + size) {
oldProps[j] = (idx - size)+'';
j++;
} else {
j++;
}
}
} else if (writeset[i].unshift != undefined) {
var at = writeset[i].unshift;
var size = writeset[i].size;
var j = 0;
var inserted = false;
while (true) {
if (j === oldProps.length) break;
var idx = +oldProps[j];
if (!inserted) {
if (idx >= at) {
for (var k = size - 1 ; k >= 0; k--)
oldProps.splice(j, 0, (at + k) + '')
inserted = true;
j += size;
} else {
j ++;
}
} else {
oldProps[j] = (idx + size) + '';
j++;
}
}
if (!inserted) {
for (var k = 0 ; k < size; k++)
oldProps.push((at+k)+'');
}
}
}
// Pop oldProps that are bigger than current length
var pop = 0;
for (var i = oldProps.length-1; i >= 0; i--) {
if (+oldProps[i] >= obj.length) {
pop++;
} else {
break;
}
}
if (pop > 0) {
t.addShift(obj, -1, (+oldProps[oldProps.length - 1]) - obj.length +1);
while (pop > 0) {
oldProps.pop();
pop--;
}
}
// Write any old props that have been changed
for (var i = 0; i < oldProps.length; i++) {
if (!obj.hasOwnProperty(oldProps[i])) {
t.addDelete(obj, oldProps[i]);
} else if (!tracker.isPropTracked(obj, oldProps[i])) {
t.addNew(obj, oldProps[i], obj[oldProps[i]]);
t.track(obj, oldProps[i]);
}
}
// Add new props
var newProps = Object.keys(obj);
for (var i = 0; i < newProps.length; i++) {
var idx = oldProps.indexOf(newProps[i]);
if (idx === -1) {
t.addNew(obj, newProps[i], obj[newProps[i]]);
t.track(obj, newProps[i]);
}
}
if (t.hasChanges())
t.uprev(obj);
t.tc().disable--;
}
}
} // mtx
} // shared