import booleanOps from './op/boolean';
import objectOps from './op/object';
import numberOps from './op/number';
import listOps from './op/list';
const _uniqueId = function _uniqueId() {
let i = 1;
return function (prefix='') {
return prefix + i++;
};
}();
const _isEmptyObj = function _isEmptyObj(obj) {
for(const key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
};
const _objectValues = function _objectValues(obj) {
if (Object.values) {
return Object.values(obj);
}
const values = [];
for (let key in obj){
if (obj.hasOwnProperty(key)) {
values.push(obj[key]);
}
}
return values;
};
/**
* @constructor
*
* @desc A Slot could be created in 2 methods:
*
* * new Slot(value)
*
* this will make a data slot
*
* * new Slot(valueFunc, followings)
*
* this will make a follower slot, where followings is an Array.
* if the element (say *following*) in observables is a:
*
* * Slot
*
* if *following* changed, *follower* will be re-evaludated by executing *valueFunc*,
* following.val() will be used as valueFunc's argument.
* its new value is the return value of *valueFunc*, and change will be propogated to
* *follower*'s followers.
*
* * not Slot
*
* when *follower* is re-evaluated, following will be used as *valueFunc*'s argument directly.
*
* and valueFunc will accept 2 parameters:
*
* * the current value of observables
* * mutation process context, it has two keys:
*
* * roots - the mutation process roots, namely, those changed by clients (api caller)
* directly
*
* * involved - the observed involed in this mutation process
*
* the context is very useful if the evaluation function needs to return value
* according to which of its followings mutated
*
* let's see two example:
*
* ```javascript
*
* const $$following = Slot(1);
* const $$follower = Slot((following, n) => following + n, [$$following, 2]);
* console.log($$follower.val()); // output 3, since n is always 2
*
* $$following.inc();
* console.log($$follower.val()); // output 4, since n is always 2
* ```
*
* ```javascript
*
* const $$a = Slot(1).tag('a');
* const $$b = Slot(([a]) => a + 1, [$$a]).tag('b');
* const $$c = Slot(2).tag('c');
* const $$d = Slot(function ([a, b], {roots, involved}) {
* console.log(roots.map(it => it.tag())); // output [a]
* console.log(involved.map(it => it.tag())); // output [b]
* return a + b;
* });
*
* // a is root of mutation proccess, and c is not changed in this mutation proccess
* $$a.inc();
*
* ```
*
* */
export const Slot = function Slot(...args) {
if (!(this instanceof Slot)) {
return new Slot(...args);
}
this._id = _uniqueId();
this._changeCbs = [];
this._followings = [];
this._followerMap = {};
// offsprings are all direct or indirect followers
this._offspringMap = {};
this._offspringLevels = [];
this._tag = '';
Object.defineProperty(this, 'token', {
get: function get() {
return this._tag + '-' + this._id;
}
});
Object.defineProperty(this, 'followings', {
get: function get() {
return this._followings;
}
});
Object.defineProperty(this, 'followers', {
get: function get() {
return _objectValues(this._followerMap);
}
});
if (args.length <= 1) {
this._value = args[0];
} else {
const [valueFunc, followings, eager] = args;
this.follow(valueFunc, followings, eager);
}
};
/**
* test if slot observes others
* @return {boolean} true if it observes others, else false
* */
Slot.prototype.isTopmost = function isTopmost() {
return !this._followings.length;
};
/**
* Set/get tag of slot, useful when debugging.
*
* @example
* // set tag will return this
* const $$s = Slot('foo').tag('bar');
* console.log($$s.tag()); // output bar
*
* @param {(string|undefined)} v - if is string, set the tag of slot and return this,
* else return the tag
* @return {(string|Slot)}
* */
Slot.prototype.tag = function tag(v) {
if (v == void 0) {
return this._tag;
}
this._tag = v;
return this;
};
/**
* set a handler to Slot to test if slot is mutated, here is an example:
*
* @example
* let $$s1 = Slot(true);
* let $$s2 = Slot(false);
*
* let $$s3 = Slot((s1, s2) => s1 && s2, [$$s1, $$s2])
* .mutationTester((oldV, newV) => oldV != newV);
*
* $$s4 = $$s3.makeFollower((s3) => !s3)
* .change(function () {
* console.log('s4 changed!');
* });
*
* // $$s2 will be changed to true, but $$s3 is not changed,
* // neither $$s4 will be changed
* $$s2.toggle();
*
*
* @param {function} tester - a handler to test if slot is changed in one mutation
* process, if a slot finds all its dependents are unmutation, the mutation process
* stops from it.
* A propriate tester will improve the performance dramatically sometimes.
*
* it access slot as *this* and slot's new value and old value as arguments,
* and return true if slot is changed in mutation process, else false.
*
* */
Slot.prototype.mutationTester = function mutationTester(tester) {
this._mutationTester = tester;
return this;
};
/**
* add a change handler
*
* !!!Warning, this is a very dangerous operation if you modify slots in
* change handler, consider the following scenario:
*
* ```javascript
* let $$s1 = Slot(1);
* let $$s2 = $$s1.makeFollower(it => it * 2);
* let $$s3 = $$s1.makeFollower(it => it * 3);
* $$s2.change(function () {
* $$s1.val(3); // forever loop
* ));
*
* $$s1.val(2);
* ```
*
*
* as a thumb of rule, don't set value for followings in change handler
*
* @param {function} proc - it will be invoked when slot is mutated in one
* mutation process the same order as it is added, it accepts the following
* parameters:
*
* * new value of Slot
* * the old value of Slot
* * the mutation context
*
* for example, you could refresh the UI, when ever the final view changed
*
* @return {Slot} this
*
* */
Slot.prototype.change = function (proc) {
this._changeCbs.push(proc);
return this;
};
/**
* remove the change handler
*
* @see {@link Slot#change}
* */
Slot.prototype.offChange = function (proc) {
this._changeCbs = this._changeCbs.filter(cb => cb != proc);
};
Slot.prototype.clearChangeCbs = function clearChangeCbs() {
this._changeCbs = [];
};
/**
* detach the target slot from its followings, and let its followers
* connect me(this), just as if slot has been eliminated after the detachment.
* this method is very useful if you want to change the dependent graph
*
* !!NOTE this method will not re-evaluate the slot and starts the mutation process
* at once, so remember to call touch at last if you want to start a mutaion process
*
* @param {Slot} targetSlot
* @return {Slot} this
*
* */
Slot.prototype.override = function override(targetSlot) {
for (let following of targetSlot._followings) {
delete following._followerMap[targetSlot._id];
}
for (let followerId in targetSlot._followerMap) {
let follower = targetSlot._followerMap[followerId];
this._followerMap[followerId] = follower;
for (let i = 0; i < follower._followings.length; ++i) {
if (follower._followings[i]._id == targetSlot._id) {
follower._followings[i] = this;
break;
}
}
}
this._offspringMap = this._offspringLevels = void 0;
// make ancestors _offspringMap obsolete, why not just calculate _offspringMap
// for each ancestor? since this operation should be as quick as possible
// and multiple override/replaceFollowing/connect operations could be batched,
// since the calculation of springs of ancestors postponed to the moment
// when ancestor is evaluated
targetSlot._getAncestors().forEach(function (ancestor) {
ancestor._offspringLevels = ancestor._offspringMap = void 0;
});
return this;
};
/**
* replaceFollowing, why not just re-follow, since follow is a quite
* expensive operation, while replaceFollowing only affect the replaced one
*
* !!NOTE this method will not re-evaluate the slot and starts the mutation process
* at once, so remember to call touch at last if you want to start a mutaion process
*
* @param idx the index of following
* @param following a slot or any object, if not provided, the "idx"th following will
* not be followed anymore.
*
* @return {Slot} this
*/
Slot.prototype.replaceFollowing = function replaceFollowing(idx, following) {
let args = [idx, 1];
if (following != void 0) {
args.push(following);
}
let [replaced] = this.followings.splice.apply(this.followings, args);
// replace the same following, just return
if (replaced == following) {
return this;
}
if (replaced instanceof Slot) {
delete replaced._followerMap[this._id];
replaced._offspringLevels = replaced._offspringMap = void 0;
replaced._getAncestors().forEach(function (ancestor) {
ancestor._offspringLevels = ancestor._offspringMap = void 0;
});
}
if (following instanceof Slot) {
following._offspringLevels = following._offspringMap = void 0;
// make ancestors _offspringMap obsolete
following._getAncestors().forEach(function (ancestor) {
ancestor._offspringLevels = ancestor._offspringMap = void 0;
});
}
return this;
};
/**
* this is the shortcut of replaceFollowing(idx)
*
* !!NOTE this method will not re-evaluate the slot and starts the mutation process
* at once, so remember to call touch at last if you want to start a mutaion process
*
* @param {number} idx - the index of
* */
Slot.prototype.removeFollowing = function removeFollowing(idx) {
return this.replaceFollowing(idx);
};
// propogate from me
Slot.prototype._propogate = function ({ roots }) {
// if has only one follower, touch it
let followers = _objectValues(this._followerMap);
if (followers.length == 0) {
return;
}
if (followers.length == 1) {
followers[0].touch(true, { roots, involved: [this] });
return;
}
if (this._offspringLevels === void 0 || this._offspringMap === void 0) {
this._setupOffsprings();
}
let cleanSlots = {};
// mutate root is always considered to be dirty,
// otherwise it won't propogate
let mutateRoot = this;
let changeCbArgs = [];
for (let level of this._offspringLevels) {
for (let follower of level) {
let involved = follower._followings.filter(function (following) {
return following instanceof Slot &&
(following._id === mutateRoot._id ||
(mutateRoot._offspringMap[following._id] && !cleanSlots[following._id]));
});
// clean follower will be untouched
let dirty = involved.length > 0;
if (!dirty) {
cleanSlots[follower._id] = follower;
continue;
}
follower.debug && console.info(`slot: slot ${follower._tag} will be refreshed`);
let context = {involved, roots};
let oldV = follower._value;
// DON'T CALL change callbacks
if (follower.touch(false, context, false)) {
changeCbArgs.push([follower, oldV, involved]);
} else {
cleanSlots[follower._id] = follower;
}
}
}
// call change callbacks at last
changeCbArgs.forEach(function ([slot, oldV, involved]) {
for (let cb of slot._changeCbs) {
cb.apply(slot, [slot._value, oldV, { involved, roots }]);
}
});
};
/**
* get or set the value, if no argument is given, get the current value of Slot,
* otherwise, set the value of Slot, *the mutation process* starts, and returns *this*
*
* @return {(any|Slot)}
* */
Slot.prototype.val = function val(...args) {
if (args.length === 0) {
if (this._value === void 0 && typeof this._valueFunc === 'function') {
this._value = this._valueFunc.apply(
this, [
this._followings.map(it => it instanceof Slot? it.val(): it),
{ roots: [ this ] },
]
);
}
return this._value;
}
return this.setV(args[0]);
};
/**
* set the slot's value, and starts a *mutation process*
*
* @param {any} newV - the new value of slot,
* */
Slot.prototype.setV = function setV(newV) {
if (typeof this._mutationTester === 'function' && !this._mutationTester(this._value, newV)) {
return this;
}
this.debug && console.info(
`slot: slot ${this._tag} mutated -- `, this._value, '->', newV
);
let oldV = this._value;
this._value = newV;
this._propogate({ roots: [this] });
for (let cb of this._changeCbs) {
cb.apply(this, [this._value, oldV, {
roots: [this],
}]);
}
return this;
};
const _colletFollowers = function _colletFollowers(slots) {
let ret = {};
for (let o of slots) {
for (let k in o._followerMap) {
let follower = o._followerMap[k];
ret[follower._id] = follower;
}
}
return _objectValues(ret);
};
Slot.prototype._setupOffsprings = function () {
this._offspringMap = {};
this._offspringLevels = [];
if (_isEmptyObj(this._followerMap)) {
return this;
}
// level by level
for (
let _offspringMap = _objectValues(this._followerMap), level = 1;
_offspringMap.length;
_offspringMap = _colletFollowers(_offspringMap), ++level
) {
for (let i of _offspringMap) {
if (!(i._id in this._offspringMap)) {
this._offspringMap[i._id] = {
slot: i,
level: level
};
} else {
this._offspringMap[i._id].level = Math.max(
this._offspringMap[i._id].level, level
);
}
}
}
let currentLevel = 0;
let slots;
for (
let { slot, level } of
_objectValues(this._offspringMap).sort((a, b) => a.level - b.level)
) {
if (level > currentLevel) {
slots = [];
this._offspringLevels.push(slots);
currentLevel = level;
}
slots.push(slot);
}
return this;
};
/**
* touch a slot, that means, re-evaluate the slot's value forcely, and
* starts *mutation process* and call change callbacks if neccessary.
* usually, you don't need call this method, only when you need to mutate the
* following graph (like override, replaceFollowing, follow)
*
* @param propogate - if starts a *mutation process*, default is true
* @param context - if null, the touched slot is served as roots, default is null
* @param callChangeCbs - if call change callbacks, default is true
*
* @return {boolean} - return true if this Slot is mutated, else false
*
* @see Slot#override
* */
Slot.prototype.touch = function (propogate=true, context=null, callChangeCbs=true) {
let oldValue = this._value;
if (!context) {
context = { roots: [this] };
}
if (this._valueFunc) {
let args = [
this._followings.map(following => following instanceof Slot? following.val(): following),
context,
];
this._value = this._valueFunc.apply(this, args);
}
if (typeof this._mutationTester == 'function' && !this._mutationTester(oldValue, this._value)) {
return false;
}
if (callChangeCbs) {
for (let cb of this._changeCbs) {
cb.apply(this, [this._value, oldValue, context]);
}
}
propogate && this._propogate({ roots: context.roots });
return true;
};
/**
* make a follower slot of me. this following has only one followings it is me.
* @example
* const $$s1 = Slot(1);
* const $$s2 = $$s1.fork(n => n + 1);
*
* is equivalent to:
*
* @example
* const $$s1 = Slot(1);
* const $$s2 = Slot(([n]) => n + 1, [$$s1]);
*
* @param {function} func - the evaluation function
* */
Slot.prototype.fork = function (func) {
return Slot(function ([following]) {
return func(following);
}, [this]);
};
/**
* unfollow all the followings if any and follow the new followings using the new
* valueFunc, this method will mutate the following graph.
*
* !!NOTE this method will not re-evaluate the slot and starts the mutation process
* at once, so remember to call touch at last if you want to start a mutaion process
*
* @param {function} valueFunc
* @param {array} followings - please see Slot's constructor
*
* @return {Slot} this
*
* @see {@link Slot}
* */
Slot.prototype.follow = function (valueFunc, followings) {
// if connect to the same followings, nothing happens
let connectTheSameFollowings = true;
for (let i = 0; i < Math.max(followings.length, this._followings.length); ++i) {
if (followings[i] != this._followings[i]) {
connectTheSameFollowings = false;
break;
}
}
if (connectTheSameFollowings && (valueFunc == this._valueFunc)) {
return this;
}
let self = this;
// make my value invalid
self._value = void 0;
self._valueFunc = valueFunc;
// affected followings slots
let affected = {};
for (let slot of followings) {
if (slot instanceof Slot) {
affected[slot._id] = slot;
}
}
for (let following of self._followings) {
if (following instanceof Slot) {
if (followings.every(function (s) {
return s !== following;
})) {
affected[following._id] = following;
delete following._followerMap[self._id];
}
}
}
// setup followings
self._followings = [];
followings.forEach(function (slot) {
self._followings.push(slot);
if (slot instanceof Slot) {
slot._followerMap[self._id] = self;
}
});
// make ancestors' _offspringMap obsolete, it will be
// recalculated until they are evaluated
self._getAncestors().forEach(function (ancestor) {
ancestor._offspringLevels = ancestor._offspringMap = void 0;
});
return self;
};
Slot.prototype._getAncestors = function _getAncestors() {
let ancestors = {};
for (let following of this._followings) {
if (following instanceof Slot) {
if (!ancestors[following._id]) {
ancestors[following._id] = following;
for (let ancestor of following._getAncestors()) {
ancestors[ancestor._id] = ancestor;
}
}
}
}
return _objectValues(ancestors);
};
/**
* shrink to a data slot with value *val*
* @return {Slot} this
* */
Slot.prototype.shrink = function (val) {
this._valueFunc = void 0;
return this.follow(void 0, []).val(val);
};
/**
* mutate a group of slots by applying functions upon them, and starts a
* *mutation proccess* whose roots are these slots to be changed
*
* NOTE!!! this is not the same as set value for each slot one by one, but
* consider them as a whole to find the best mutaion path
*
* @example
* let $$p1 = Slot(1).tag('p1');
* let $$p2 = Slot(2).tag('p2');
* let $$p3 = $$p2.fork(it => it + 1).tag('p3');
* let $$p4 = Slot(function ([p1, p2, p3], { roots, involved }) {
* console.log(roots.map(it => it.tag())); // p1, p2
* console.log(involved.map(it => it.tag())); // p1, p2, p3
* return p1 + p2 + p3;
* }, [$$p1, $$p2, $$p3]);
* rimple.mutateWith([
* [$$p1, n => n + 1],
* [$$p2, n => n + 2],
* ]);
* console.log($$p1.val(), $$p2.val(), $$p3.val(), $$p4.val()); // 2, 4, 5, 11
*
* @param {array} slotValuePairs - each element is an array, whose first value is
* a Slot, and second is the function to be applied
*
* */
export const mutateWith = function mutateWith(slotFnPairs) {
return mutate(slotFnPairs.map(function ([slot, fn]) {
return [slot, fn && fn.apply(slot, [slot.val()])];
}));
};
/**
* mutate a group of slots, and starts ONE *mutation proccess* whose
* roots are these slots to be changed.
*
* NOTE!!! this is not the same as set value for each slot one by one, but
* consider them as a whole to find the best mutaion path
*
* @example
* let $$p1 = Slot(1).tag('p1');
* let $$p2 = Slot(2).tag('p2');
* let $$p3 = $$p2.fork(it => it + 1).tag('p3');
* let $$p4 = Slot(function ([p1, p2, p3], { roots, involved }) {
* console.log(roots.map(it => it.tag())); // p1, p2
* console.log(involved.map(it => it.tag())); // p1, p2, p3
* return p1 + p2 + p3;
* }, [$$p1, $$p2, $$p3]);
* rimple.mutate([
* [$$p1, 2],
* [$$p2, 4],
* ]);
* console.log($$p1.val(), $$p2.val(), $$p3.val(), $$p4.val()); // 2, 4, 5, 11
*
* @param {array} slotValuePairs - each element is an array, whose first value is
* a Slot, and second is the new value of slots
*
* */
export const mutate = function (slotValuePairs) {
let cleanSlots = {};
let roots = slotValuePairs.map(([slot]) => slot);
// mutate the targets directly
slotValuePairs.forEach(function ([slot, value]) {
slot.debug && console.info(`slot ${slot._tag} mutationTester`, slot._value, value);
let oldValue = slot._value;
if (value !== void 0) {
slot._value = value;
if (slot._mutationTester && !slot._mutationTester(oldValue, value)) {
cleanSlots[slot._id] = slot;
return;
}
}
for (let cb of slot._changeCbs) {
cb.call(slot, slot._value, oldValue, { roots });
}
});
// related slots include roots
let relatedSlots = {};
let addToRelatedSlots = function (slot, level) {
if (slot._id in relatedSlots) {
relatedSlots[slot._id].level = Math.max(
level, relatedSlots[slot._id].level
);
} else {
relatedSlots[slot._id] = {
slot,
level,
};
}
};
slotValuePairs.forEach(function ([slot]) {
addToRelatedSlots(slot, 0);
if (slot._offspringMap === void 0) {
slot._setupOffsprings();
}
_objectValues(slot._offspringMap).forEach(function ({slot: offspring, level}) {
addToRelatedSlots(offspring, level);
});
});
// group _offspringMap by level, but omits level 0 (those mutated directly)
// since they have been touched
let slots;
let levels = [];
let currentLevel = 0;
_objectValues(relatedSlots)
.sort((a, b) => a.level - b.level)
.filter(it => it.level > 0)
.forEach(function ({slot, level}) {
if (level > currentLevel) {
slots = [];
levels.push(slots);
currentLevel = level;
}
slots.push(slot);
});
let changeCbArgs = [];
for (let level of levels) {
for (let follower of level) {
let involved = follower._followings.filter(function (p) {
return p instanceof Slot && relatedSlots[p._id] && !cleanSlots[p._id];
});
if (!involved.length) {
cleanSlots[follower._id] = follower;
continue;
}
follower.debug && console.info(
`slot: slot ${follower._tag} will be refreshed`
);
let context = { involved, roots };
// DON'T use val(), val will reevaluate this slot
let oldV = follower._value;
// DON'T CALL change callbacks
if (follower.touch(false, context, false)) {
changeCbArgs.push([follower, oldV, involved]);
} else {
cleanSlots[follower._id] = follower;
}
}
}
// call change callbacks at last
changeCbArgs.forEach(function ([slot, oldV, involved]) {
for (let cb of slot._changeCbs) {
cb.apply(slot, [slot._value, oldV, { involved, roots }]);
}
});
};
/**
* apply the function to me
*
* @example
* const $$s = Slot(1);
* $$s.mutateWith(function (s, n) {
* return s + n;
* }, [2]);
* console.log($$s.val()); // output 3
*
* is equivalent to
* @example
* const $$s = Slot(1);
* $$s.val(function (s, n) { return s + n; }($$s.val(), 2));
*
* @param {function} func - the mutation function
* @param {array} args - the extra arguments provided to func, default is []
*
* @return {Slot} this
*
* */
Slot.prototype.mutateWith = function mutateWith(func, args=[]) {
args = [this._value].concat(args);
return this.val(func.apply(this, args));
};
/**
* add methods to Slot's prototype
*
* @example
* rimple.mixin({
* negate() {
* return this.val(-this.val());
* }
* });
* const $$s = Slot(1).negate();
* console.log($$s.val()); // output -1
*
* @param {object} mixins - the mixins to be added
*
* */
export const mixin = function mixin(mixins) {
Object.assign(Slot.prototype, mixins);
};
/**
* create an immutable slot, which use '===' to test if value is mutated
* */
export const immSlot = function (value) {
return Slot(value).mutationTester(function (a, b) {
return a !== b;
});
};
mixin(booleanOps);
mixin(objectOps);
mixin(numberOps);
mixin(listOps);
export const slot = Slot;