UNPKG

shared-updated

Version:

Modern fork of shared (Kevin Jones), updated for latest Node.js and MongoDB

684 lines (598 loc) 20.7 kB
// 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='utils.ts' /> /// <reference path='id.ts' /> /// <reference path='types.ts' /> /// <reference path='serial.ts' /> /* * Tracking provides a core service to enabling monitoring of how objects * an arrays are changed over some period. It has similar motives to the * proposed Object.observe model but is specifically designed to be * node portable & suitable for distributed transactions. * * This code generates raw tracking logs. They need post-processing for * most use cases, see mtx.ts for code that does this in this case. */ module shared { export module tracker { var Buffer = require('buffer'); /* * Tracker cache interface. Used to store change details that are * generated when traversing/changing tracked objects & arrays. * The concrete implementation is in mtx.ts. * * There is no handling of new objects here as they are discovered * after the fact when changes are examined. This is just the minimum * interface needs by the tracking code and its unit tests. */ export interface TrackCache extends serial.ReferenceHandler { /* * If >0 then tracking should be disabled. This is only for * internal code to make sure it is not creating false * tracking data when manipulating the tracked objects/arrays. */ disable: number; /* * Mark an object as read during tracking. It is assumed the * object has a tracker that uses this TrackCache. */ markRead(value: any) : void; /* * Record a modification on an object. The lasttx argument stores * a unique id for the previous modification. The id of the current * modification is returned for use on the next call. This makes it * easy to create linked lists of changes for a specific object to * aid post-processing speed. */ addNew(obj: any, prop: string, value: any, lasttx: number) : number; addWrite(obj: any, prop: string, value: any, last: any, lasttx: number) : number; addDelete(obj: any, prop: string, lasttx: number): number; addReverse(obj: any, lasttx: number): number; addSort(obj: any, lasttx: number): number; addShift(obj: any, at:number, size:number, lasttx: number): number; addUnshift(obj: any, at:number, size:number, lasttx: number): number; } /* * Exception for indicating the cache is missing an object * needed for navigation. */ export class UnknownReference { private _id: string; // Id of containing object private _prop: string; // Name of property wit private _missing: any; // Id of missing object constructor (id, prop, missing) { this._id = id; this._prop = prop; this._missing = missing; } id() { return this._id; } prop() { return this._prop; } missing() { return this._missing; } } /* * Recover the tracker for an object/array, may return null */ export function getTrackerUnsafe(value: any) : Tracker { if (value._tracker === undefined) return null; return value._tracker; } /* * Recover the tracker for an object/array */ export function getTracker(value: any) : Tracker { utils.dassert(utils.isObject(value._tracker)); return value._tracker; } /* * Test if object is tracked */ export function isTracked(value: any) : bool { return utils.isObject(value._tracker); } /* * Object/Array tracker. Construct this over an object/array and it will * attach itself to that object/array as a non-enumerable '_tracker' property. * This is kind of odd, but saves doing object->tracker lookups. The downside * is to avoid a circular ref many tracker methods must be passed the objects * they are tracking as this is not recorded in the tracker itself. * * The tracker wraps the enumerable properties of the object/array so that * it can log reads to other objects/arrays and any mutations. The log can * be accessed via changes(). * * The mechanics here are messy so I have simply tried to write this as correct * rather than as quick & correct. A bit of extra thought can probably * improve the performance a lot. */ export class Tracker { private _tc: TrackCache; private _id: utils.uid; private _rev; private _ref; private _type: types.TypeDesc; private _lastTx: number; private _userdata: any; constructor(tc: TrackCache, obj: any, id: utils.uid = utils.UID(), rev?: number) { utils.dassert(utils.isObject(tc)); utils.dassert(utils.isUID(id)); // Error check if (obj === null || typeof (obj) !== 'object') utils.defaultLogger().fatal('Trying to track non-object/array type'); if (obj.hasOwnProperty('_tracker')) utils.defaultLogger().fatal('Trying to track already tracked object or array'); // Init this._tc = tc; this._rev = rev || 0; this._id = id; this._lastTx = -1; this._id = id; this._type = types.TypeStore.instance().type(obj); this._userdata = null this._ref = 0; // Add tracker to object Object.defineProperty(obj, '_tracker', { value: this }); // Start tracking if (obj instanceof Array) { trackArray(obj); } for (var prop in obj) { this.track(obj, prop); } } /* * When trackers die they lose connection to the cache. Normally * they die when changes to the object can not be undone and so * the object needs to be refreshed from the master cache. */ kill() { this._tc = null; } /* * Has this tracker/object combo died */ isDead(): bool { return this._tc === null; } /** * Get the tracker cache this tracker is using */ tc() : TrackCache { return this._tc; }; /** * Get the unique object id */ id() : utils.uid { return this._id; }; /** * Get the objects (pre-changes) type */ type() : types.TypeDesc { return this._type; }; /** * Get/Increment the object revision, returning new value */ rev(by?: number) : number { if (by !== undefined) this._rev += by; return this._rev; }; /** * Set object rev to a value, must be >= to existing rev */ setRev(to: number) : number { if (to >= this._rev) { this._rev = to; } return this._rev; }; /** * Get/Increment the object revision, returning new value */ ref(by?: number) : number { if (by !== undefined) this._ref += by; return this._ref; }; /** * Set object rev to a value, must be >= to existing rev */ setRef(to: number) : number { if (to >= this._ref) { this._ref = to; } return this._ref; }; /* * Set a user supplied data object */ setData(ud: any) { this._userdata = ud; } /* * Get a user supplied data object */ getData() : any { return this._userdata; } /* * Has a change been recorded against the object */ hasChanges() : bool { return (this._lastTx != -1); } /* * The index of the last recorded change in the mtx */ lastChange(): number { return this._lastTx; } /* * Update the index of the last recorded change in the mtx */ setLastChange(tx: number): void { this._lastTx = tx; } /** * Change notification handlers called to record changes */ addNew(obj: any, prop: string, value: any) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addNew(obj, prop, value, this._lastTx); }; addWrite(obj: any, prop: string, value: any, last: any) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addWrite(obj, prop, value, last, this._lastTx); }; addDelete(obj: any, prop: string) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addDelete(obj, prop, this._lastTx); } addReverse(obj: any) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addReverse(obj, this._lastTx); } addSort(obj: any) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addSort(obj, this._lastTx); } addShift(obj: any, at: number, size: number) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addShift(obj, at, size, this._lastTx); } addUnshift(obj: any, at:number, size: number) { utils.dassert(getTracker(obj) === this); this._lastTx = this.tc().addUnshift(obj, at, size, this._lastTx); } /* * Make sure all properties are being tracked */ retrack(obj: any) { utils.dassert(getTracker(obj) === this); for (var prop in obj) { if (!isPropTracked(obj, prop)) { this.track(obj, prop); } } this._type = types.TypeStore.instance().type(obj); } /** * Wrap a property for get/set tracking */ track(obj: any, prop: string) : void { utils.dassert(getTracker(obj) === this); if (obj.hasOwnProperty(prop)) { var value = obj[prop]; if (delete obj[prop]) { wrapProp(obj, prop, value); } else { throw new Error('Unwrappable property found: ' + prop); } } } /** * Uprev an object recording new properties */ uprev(obj) { utils.dassert(getTracker(obj) === this); this._rev += 1; this._type = types.TypeStore.instance().type(obj); } /** * Down rev (undo) an object recording new properties */ downrev(obj) { utils.dassert(getTracker(obj) === this); if (this._lastTx !== -1) { this._lastTx = -1; this._rev -= 1; this._type = types.TypeStore.instance().type(obj); } } } /* * Utility methods that aid the tracker. */ function lexSort(a, b) { var astr = a.toString(); var bstr = b.toString(); if (astr < bstr) return -1; if (astr > bstr) return 1; return 0; } function trackArray(arr) { Object.defineProperty(arr, 'shift', { enumerable: false, configurable: false, value: function () { var t = getTracker(arr); if (t.tc().disable === 0) { t.tc().disable++; // Shift will 'untrack' our props so we have to record what // is currently being tracked and reapply this after the shift // Sad I know, but just how it works. var k = Object.keys(arr); var tracked = []; k.forEach(function (e, i, a) { tracked.push(isPropTracked(arr, a[i])); }); // Record & perform the shift if (arr.length > 0) { t.addShift(arr,0,1); } var r = Array.prototype.shift.apply(arr, arguments); // Restore tracking var k = Object.keys(arr); for (var i = 0; i < arr.length; i++) { var key = k[i]; if (tracked[i + 1] && !isPropTracked(arr, key)) t.track(arr, key); } t.tc().disable--; return r; } else { return Array.prototype.shift.apply(arr, arguments); } } }); Object.defineProperty(arr, 'unshift', { enumerable: false, configurable: false, value: function () { var t = getTracker(arr); if (t.tc().disable === 0) { t.tc().disable++; // Cache which props are tracked var k = Object.keys(arr); var tracked = []; k.forEach(function (e, i, a) { tracked.push(isPropTracked(arr, a[i])); }); // Record the unshift if (arguments.length > 0) { t.addUnshift(arr, 0, arguments.length); } var r = Array.prototype.unshift.apply(arr, arguments); // Record writes of new data for (var i = 0; i < arguments.length; i++) { t.track(arr, i + ''); t.addNew(arr, i + '', arr[i]); } // Restore our tracking var k = Object.keys(arr); for (; i < arr.length; i++) { var key = k[i]; if (tracked[i - arguments.length] && !isPropTracked(arr, key)) t.track(arr, key); } t.tc().disable--; return r; } else { return Array.prototype.unshift.apply(arr, arguments); } } }); Object.defineProperty(arr, 'reverse', { enumerable: false, configurable: false, value: function () { var t = getTracker(arr); if (t.tc().disable === 0) { t.tc().disable++; // Reverse keeps the tracking but does not reverse it leading // to lots of confusion, another hack required var k = Object.keys(arr); var tracked = []; k.forEach(function (e, i, a) { tracked.push(isPropTracked(arr, a[i])); }); tracked.reverse(); // Record & perform the reverse t.addReverse(arr); var r = Array.prototype.reverse.apply(arr, arguments); // Recover tracking state var k = Object.keys(r); for (var i = 0; i < k.length; i++) { var key = k[i]; var trckd = isPropTracked(arr, key); if (tracked[i] && !trckd) { t.track(arr, key); } else if (!tracked[i] && trckd) { unTrack(arr, key); } } t.tc().disable--; return r; } else { return Array.prototype.reverse.apply(arr, arguments); } } }); Object.defineProperty(arr, 'sort', { enumerable: false, configurable: false, value: function () { var t = getTracker(arr); if (t.tc().disable === 0) { t.tc().disable++; // Now we are in trouble, sort is like reverse, it leaves tracking // at the pre-sort positions and we need to correct this by sorting // over a wrapper array and then storing the results. var k = Object.keys(arr); var pairs = []; k.forEach(function (e, i, a) { pairs.push({ elem: arr[a[i]], track: isPropTracked(arr, a[i]) }); }); // Run the sort var sortfn = arguments[0]; if (sortfn === undefined) sortfn = lexSort; var wrapFn = function (a, b) { var r = sortfn(a.elem, b.elem); return r; }; Array.prototype.sort.apply(pairs, [wrapFn]); // Apply results for (var i = 0; i < pairs.length; i++) { var key = k[i]; arr[key] = pairs[i].elem; var trckd = isPropTracked(arr, key); if (pairs[i].track && !trckd) { t.track(arr, key); } else if (!pairs[i].track && trckd) { unTrack(arr, key); } } // Best record it after all that t.addSort(arr); t.tc().disable--; return arr; } else { Array.prototype.sort.apply(pairs, [wrapFn]); } } }); Object.defineProperty(arr, 'splice', { enumerable: false, configurable: false, value: function () { var t = getTracker(arr); if (t.tc().disable === 0) { t.tc().disable++; // ES5 15.4.4.12 + Moz extension (What a mess!) var len = arr.length; var relStart = utils.toInteger(arguments[0]); var actStart; if (relStart < 0) actStart = Math.max((len + relStart), 0); else actStart = Math.min(relStart, len); var actDelCount = len - actStart; if (arguments[1] !== undefined) actDelCount = Math.min(Math.max(utils.toInteger(arguments[1]), 0), len - actStart); var insCount = Math.max(arguments.length-2,0); // Splice leaves tracking where it was and does not adjust so we have to // correct manually as usual but remebering about sparse arrays var k = Object.keys(arr); var tracked = []; k.forEach(function (e, i, a) { tracked.push(isPropTracked(arr, a[i])); }); var r = Array.prototype.splice.apply(arr, arguments); // Now recover correct tracking state & record changes //var k = Object.keys(r); for (var i = 0; i < k.length; i++) { var key = +k[i]; if (key < actStart || key >= actStart + actDelCount) { if (key >= actStart) { key += (insCount - actDelCount); } var skey = key + ''; var trckd = isPropTracked(arr, skey); if (tracked[i] && !trckd) { t.track(arr, skey); } else if (!tracked[i] && trckd) { unTrack(arr, skey); } } } // Anything inserted should not be tracked for (var i = actStart; i < actStart + insCount; i++) { if (arr[i] !== undefined) unTrack(arr, i + ''); } if (actDelCount>0) t.addShift(arr, actStart, actDelCount); if (insCount>0) t.addUnshift(arr, actStart, insCount); t.tc().disable--; return r; } else { return Array.prototype.splice.apply(arr, arguments); } } }); } function wrapProp(obj: any, prop: string, value: any): void { var tracker = getTracker(obj); Object.defineProperty(obj, prop, { enumerable: true, configurable: true, get: function () { if (tracker.tc().disable === 0) { if (value !== null && typeof value === 'object') { if (value instanceof serial.Reference) { var ref: serial.Reference = value; throw new UnknownReference(tracker.id(), prop, ref.id()); } var t = getTrackerUnsafe(value); if (t!==null) { if (t.isDead()) { throw new UnknownReference(tracker.id(), prop, t.id()); } else { tracker.tc().markRead(value); } } } } return value; }, set: function (setValue) { if (tracker.tc().disable === 0) { tracker.tc().disable++; tracker.addWrite(obj, prop, setValue, value); tracker.tc().disable--; } value = setValue; } }); } // TODO: Is this the best/only option? export function isPropTracked(obj: any, prop: string) : bool { var desc = Object.getOwnPropertyDescriptor(obj, prop); return (desc.get != undefined && desc.set != undefined); } function unTrack(obj: any, prop: string) { var desc = Object.getOwnPropertyDescriptor(obj, prop); if (desc.get != undefined && desc.set != undefined) { var v = obj[prop]; Object.defineProperty(obj, prop, { value: v }); } } } // tracker } //shared