preservative
Version:
A proxy for state machines to preserve state and history
104 lines (89 loc) • 2.57 kB
JavaScript
var slice = Array.prototype.slice;
var construct = function (Ctor, args) {
var F;
F = function () {
return Ctor.apply(this, args);
};
F.prototype = Ctor.prototype;
return new F();
};
var getKeysDeep = function (inst) {
var keys = Object.keys(inst);
for (var obj = inst; obj !== null; obj = Object.getPrototypeOf(obj)) {
var currKeys = Object.keys(obj);
for (var i = 0; i < currKeys.length; i += 1) {
var key = currKeys[i];
if (keys.indexOf(key) < 0) {
keys.push(key);
}
}
}
return keys;
};
var validOp = function (op) {
return 'string' === typeof op.type && Array.isArray(op.args);
};
module.exports = function (Klass, apiList, opts) {
opts = opts || {};
// create a wrapper class
function SubKlass() {
this.state = [{
type: 'new',
args: slice.call(arguments, 0)
}];
var inst = construct(Klass, arguments);
this.inst = inst;
getKeysDeep(inst).filter(function (key) {
// never allow overriding the ones we are keeping track of
return apiList.indexOf(key) < 0;
}).forEach(function (key) {
if (typeof inst[key] === 'function') {
// forward methods
this[key] = inst[key].bind(inst);
}
else {
// proxy variables
Object.defineProperty(this, key, {
get: function () {
return inst[key];
}
});
}
}.bind(this));
}
apiList.forEach(function (name) {
SubKlass.prototype[name] = function () {
var res = this.inst[name].apply(this.inst, arguments);
if (!(!res && opts.filterNoops)) {
this.state.push({
type: name,
args: slice.call(arguments, 0)
});
}
return res;
};
});
SubKlass.prototype.preserve = function () {
return this.state.slice();
};
SubKlass.from = function (preserve) {
if (!Array.isArray(preserve)) {
throw new Error("No operations preserved");
}
if (!validOp(preserve[0]) || preserve[0].type !== 'new') {
throw new Error("First operation must be a valid 'new'" + preserve[0].type);
}
// recreate with same ctor args
var inst = construct(SubKlass, preserve[0].args);
// re-apply all worthy operations to the state machine in correct order
preserve.slice(1).forEach(function (op) {
if (!validOp(op)) {
var s = JSON.stringify(op);
throw new Error("Preserved operation " + s + " invalid");
}
inst[op.type].apply(inst, op.args);
});
return inst;
};
return SubKlass;
};