mobservable
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -1,6 +0,1 @@ | ||
/** | ||
* MOBservable | ||
* (c) 2015 - Michel Weststrate | ||
* https://github.com/mweststrate/mobservable | ||
*/ | ||
var __extends = this.__extends || function (d, b) { | ||
@@ -12,772 +7,829 @@ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; | ||
}; | ||
function createObservable(value, scope) { | ||
var prop = null; | ||
if (Array.isArray && Array.isArray(value) && mobservableStatic.debugLevel) | ||
warn("mobservable.value() was invoked with an array. Probably you want to create an mobservable.array() instead of observing a reference to an array?"); | ||
if (typeof value === "function") | ||
prop = new ComputedObservable(value, scope); | ||
else | ||
prop = new ObservableValue(value, scope); | ||
var propFunc = function (value) { | ||
if (arguments.length > 0) | ||
return prop.set(value); | ||
else | ||
return prop.get(); | ||
var mobservable; | ||
(function (mobservable) { | ||
function createObservable(value, scope) { | ||
if (Array.isArray(value)) | ||
return new ObservableArray(value); | ||
if (typeof value === "function") | ||
return mobservable.mobservableStatic.computed(value, scope); | ||
return mobservable.mobservableStatic.primitive(value); | ||
} | ||
mobservable.mobservableStatic = function (value, scope) { | ||
return createObservable(value, scope); | ||
}; | ||
propFunc.observe = prop.observe.bind(prop); | ||
propFunc.prop = prop; | ||
propFunc.toString = function () { return prop.toString(); }; | ||
return propFunc; | ||
} | ||
var mobservableStatic = function (value, scope) { | ||
return createObservable(value, scope); | ||
}; | ||
mobservableStatic.value = createObservable; | ||
mobservableStatic.debugLevel = 0; | ||
mobservableStatic.watch = function watch(func, onInvalidate) { | ||
var dnode = new DNode(true); | ||
var retVal; | ||
dnode.nextState = function () { | ||
retVal = func(); | ||
dnode.nextState = function () { | ||
dnode.dispose(); | ||
onInvalidate(); | ||
return false; | ||
}; | ||
return false; | ||
mobservable.mobservableStatic.value = createObservable; | ||
mobservable.mobservableStatic.primitive = mobservable.mobservableStatic.reference = function (value) { | ||
return new ObservableValue(value).createGetterSetter(); | ||
}; | ||
dnode.computeNextState(); | ||
return [retVal, function () { return dnode.dispose(); }]; | ||
}; | ||
mobservableStatic.observeProperty = function observeProperty(object, key, listener, invokeImmediately) { | ||
if (invokeImmediately === void 0) { invokeImmediately = false; } | ||
if (!object || !key || object[key] === undefined) | ||
throw new Error("Object '" + object + "' has no key '" + key + "'."); | ||
if (!listener || typeof listener !== "function") | ||
throw new Error("Third argument to mobservable.observeProperty should be a function"); | ||
var currentValue = object[key]; | ||
if (currentValue instanceof ObservableValue || currentValue instanceof ObservableArray) | ||
return currentValue.observe(listener, invokeImmediately); | ||
else if (currentValue.prop && currentValue.prop instanceof ObservableValue) | ||
return currentValue.prop.observe(listener, invokeImmediately); | ||
var observer = new ComputedObservable((function () { return object[key]; }), object); | ||
var disposer = observer.observe(listener, invokeImmediately); | ||
if (mobservableStatic.debugLevel && observer.dependencyState.observing.length === 0) | ||
warn("mobservable.observeProperty: property '" + key + "' of '" + object + " doesn't seem to be observable. Did you define it as observable?"); | ||
return once(function () { | ||
disposer(); | ||
observer.dependencyState.dispose(); | ||
}); | ||
}; | ||
mobservableStatic.array = function array(values) { | ||
return new ObservableArray(values); | ||
}; | ||
mobservableStatic.toJSON = function toJSON(value) { | ||
if (value instanceof ObservableArray) | ||
return value.values(); | ||
return value; | ||
}; | ||
mobservableStatic.batch = function batch(action) { | ||
return Scheduler.batch(action); | ||
}; | ||
mobservableStatic.onReady = function onReady(listener) { | ||
return Scheduler.onReady(listener); | ||
}; | ||
mobservableStatic.onceReady = function onceReady(listener) { | ||
Scheduler.onceReady(listener); | ||
}; | ||
mobservableStatic.observable = function observable(target, key, descriptor) { | ||
var baseValue = descriptor ? descriptor.value : null; | ||
if (typeof baseValue === "function") { | ||
delete descriptor.value; | ||
delete descriptor.writable; | ||
descriptor.get = function () { | ||
mobservableStatic.defineObservableProperty(this, key, baseValue); | ||
return this[key]; | ||
}; | ||
descriptor.set = function () { | ||
console.trace(); | ||
throw new Error("It is not allowed to reassign observable functions"); | ||
}; | ||
} | ||
else { | ||
Object.defineProperty(target, key, { | ||
configurable: true, enumberable: true, | ||
get: function () { | ||
mobservableStatic.defineObservableProperty(this, key, undefined); | ||
mobservable.mobservableStatic.computed = function (func, scope) { | ||
return new ComputedObservable(func, scope).createGetterSetter(); | ||
}; | ||
mobservable.mobservableStatic.array = function array(values) { | ||
return new ObservableArray(values); | ||
}; | ||
mobservable.mobservableStatic.props = function props(target, props, value) { | ||
switch (arguments.length) { | ||
case 0: | ||
throw new Error("Not enough arguments"); | ||
case 1: | ||
return mobservable.mobservableStatic.props(target, target); | ||
case 2: | ||
for (var key in props) | ||
mobservable.mobservableStatic.props(target, key, props[key]); | ||
break; | ||
case 3: | ||
var isArray = Array.isArray(value); | ||
var observable = mobservable.mobservableStatic.value(value, target); | ||
Object.defineProperty(target, props, { | ||
get: isArray | ||
? function () { return observable; } | ||
: observable, | ||
set: isArray | ||
? function (newValue) { observable.replace(newValue); } | ||
: observable, | ||
enumerable: true, | ||
configurable: false | ||
}); | ||
break; | ||
} | ||
return target; | ||
}; | ||
mobservable.mobservableStatic.observable = function observable(target, key, descriptor) { | ||
var baseValue = descriptor ? descriptor.value : null; | ||
if (typeof baseValue === "function") { | ||
delete descriptor.value; | ||
delete descriptor.writable; | ||
descriptor.get = function () { | ||
mobservable.mobservableStatic.props(this, key, baseValue); | ||
return this[key]; | ||
}, | ||
set: function (value) { | ||
if (Array.isArray(value)) { | ||
var ar = new ObservableArray(value); | ||
Object.defineProperty(this, key, { | ||
value: ar, | ||
writeable: false, | ||
configurable: false, | ||
enumberable: true | ||
}); | ||
}; | ||
descriptor.set = function () { | ||
console.trace(); | ||
throw new Error("It is not allowed to reassign observable functions"); | ||
}; | ||
} | ||
else { | ||
Object.defineProperty(target, key, { | ||
configurable: false, enumberable: true, | ||
get: function () { | ||
mobservable.mobservableStatic.props(this, key, undefined); | ||
return this[key]; | ||
}, | ||
set: function (value) { | ||
mobservable.mobservableStatic.props(this, key, value); | ||
} | ||
else | ||
mobservableStatic.defineObservableProperty(this, key, value); | ||
}); | ||
} | ||
}; | ||
mobservable.mobservableStatic.toPlainValue = function toPlainValue(value) { | ||
if (value) { | ||
if (value instanceof Array) | ||
return value.slice(); | ||
else if (value instanceof ObservableValue) | ||
return value.get(); | ||
else if (typeof value === "function" && value.impl) { | ||
if (value.impl instanceof ObservableValue) | ||
return value(); | ||
else if (value.impl instanceof ObservableArray) | ||
return value().slice(); | ||
} | ||
}); | ||
} | ||
}; | ||
mobservableStatic.defineObservableProperty = function defineObservableProperty(object, name, initialValue) { | ||
var _property = mobservableStatic.value(initialValue, object); | ||
definePropertyForObservable(object, name, _property); | ||
}; | ||
mobservableStatic.initializeObservableProperties = function initializeObservableProperties(object) { | ||
for (var key in object) | ||
if (object.hasOwnProperty(key)) { | ||
if (object[key] && object[key].prop && object[key].prop instanceof ObservableValue) | ||
definePropertyForObservable(object, key, object[key]); | ||
else if (typeof value === "object") { | ||
var res = {}; | ||
for (var key in value) | ||
res[key] = toPlainValue(value[key]); | ||
return res; | ||
} | ||
} | ||
}; | ||
function definePropertyForObservable(object, name, observable) { | ||
Object.defineProperty(object, name, { | ||
get: function () { | ||
return observable(); | ||
}, | ||
set: function (value) { | ||
observable(value); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
} | ||
var ObservableValue = (function () { | ||
function ObservableValue(_value, scope) { | ||
this._value = _value; | ||
this.scope = scope; | ||
this.changeEvent = new SimpleEventEmitter(); | ||
this.dependencyState = new DNode(false); | ||
} | ||
ObservableValue.prototype.set = function (value) { | ||
if (value !== this._value) { | ||
var oldValue = this._value; | ||
this.dependencyState.markStale(); | ||
this._value = value; | ||
this.dependencyState.markReady(true); | ||
this.changeEvent.emit(value, oldValue); | ||
} | ||
return this.scope; | ||
return value; | ||
}; | ||
ObservableValue.prototype.get = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._value; | ||
}; | ||
ObservableValue.prototype.observe = function (listener, fireImmediately) { | ||
var _this = this; | ||
if (fireImmediately === void 0) { fireImmediately = false; } | ||
this.dependencyState.setRefCount(+1); | ||
if (fireImmediately) | ||
listener(this.get(), undefined); | ||
var disposer = this.changeEvent.on(listener); | ||
mobservable.mobservableStatic.observeProperty = function observeProperty(object, key, listener, invokeImmediately) { | ||
if (invokeImmediately === void 0) { invokeImmediately = false; } | ||
if (!object || !key || object[key] === undefined) | ||
throw new Error("Object '" + object + "' has no property '" + key + "'."); | ||
if (!listener || typeof listener !== "function") | ||
throw new Error("Third argument to mobservable.observeProperty should be a function"); | ||
var currentValue = object[key]; | ||
if (currentValue instanceof ObservableValue || currentValue instanceof ObservableArray) | ||
return currentValue.observe(listener, invokeImmediately); | ||
else if (currentValue.impl && (currentValue.impl instanceof ObservableValue || currentValue instanceof ObservableArray)) | ||
return currentValue.impl.observe(listener, invokeImmediately); | ||
var observer = new ComputedObservable((function () { return object[key]; }), object); | ||
var disposer = observer.observe(listener, invokeImmediately); | ||
if (mobservable.mobservableStatic.debugLevel && observer.dependencyState.observing.length === 0) | ||
warn("mobservable.observeProperty: property '" + key + "' of '" + object + " doesn't seem to be observable. Did you define it as observable?"); | ||
return once(function () { | ||
_this.dependencyState.setRefCount(-1); | ||
disposer(); | ||
observer.dependencyState.dispose(); | ||
}); | ||
}; | ||
ObservableValue.prototype.toString = function () { | ||
return "Observable[" + this._value + "]"; | ||
mobservable.mobservableStatic.watch = function watch(func, onInvalidate) { | ||
var watch = new WatchedExpression(func, onInvalidate); | ||
return [watch.value, function () { return watch.dispose(); }]; | ||
}; | ||
return ObservableValue; | ||
})(); | ||
var ComputedObservable = (function (_super) { | ||
__extends(ComputedObservable, _super); | ||
function ComputedObservable(func, scope) { | ||
_super.call(this, undefined, scope); | ||
this.func = func; | ||
this.isComputing = false; | ||
this.hasError = false; | ||
if (!func) | ||
throw new Error("ComputedObservable requires a function"); | ||
this.dependencyState.isComputed = true; | ||
this.dependencyState.nextState = this.compute.bind(this); | ||
} | ||
ComputedObservable.prototype.get = function () { | ||
if (this.isComputing) | ||
throw new Error("Cycle detected"); | ||
var state = this.dependencyState; | ||
if (state.isSleeping) { | ||
if (DNode.trackingStack.length > 0) { | ||
state.wakeUp(); | ||
state.notifyObserved(); | ||
} | ||
else { | ||
this.compute(); | ||
} | ||
mobservable.mobservableStatic.batch = function batch(action) { | ||
return Scheduler.batch(action); | ||
}; | ||
mobservable.mobservableStatic.debugLevel = 0; | ||
var ObservableValue = (function () { | ||
function ObservableValue(_value) { | ||
this._value = _value; | ||
this.changeEvent = new SimpleEventEmitter(); | ||
this.dependencyState = new DNode(this); | ||
} | ||
else { | ||
state.notifyObserved(); | ||
} | ||
if (state.hasCycle) | ||
throw new Error("Cycle detected"); | ||
if (this.hasError) { | ||
if (mobservableStatic.debugLevel) { | ||
console.trace(); | ||
warn(this + ": rethrowing caught exception to observer: " + this._value + (this._value.cause || '')); | ||
ObservableValue.prototype.set = function (value) { | ||
if (value !== this._value) { | ||
var oldValue = this._value; | ||
this.dependencyState.markStale(); | ||
this._value = value; | ||
this.dependencyState.markReady(true); | ||
this.changeEvent.emit(value, oldValue); | ||
} | ||
throw this._value; | ||
}; | ||
ObservableValue.prototype.get = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._value; | ||
}; | ||
ObservableValue.prototype.observe = function (listener, fireImmediately) { | ||
var _this = this; | ||
if (fireImmediately === void 0) { fireImmediately = false; } | ||
this.dependencyState.setRefCount(+1); | ||
if (fireImmediately) | ||
listener(this.get(), undefined); | ||
var disposer = this.changeEvent.on(listener); | ||
return once(function () { | ||
_this.dependencyState.setRefCount(-1); | ||
disposer(); | ||
}); | ||
}; | ||
ObservableValue.prototype.createGetterSetter = function () { | ||
var _this = this; | ||
var self = this; | ||
var f = function (value) { | ||
if (arguments.length > 0) | ||
self.set(value); | ||
else | ||
return self.get(); | ||
}; | ||
f.observe = function (listener, fire) { return _this.observe(listener, fire); }; | ||
f.impl = this; | ||
f.toString = function () { return _this.toString(); }; | ||
return f; | ||
}; | ||
ObservableValue.prototype.toString = function () { | ||
return "Observable[" + this._value + "]"; | ||
}; | ||
return ObservableValue; | ||
})(); | ||
var ComputedObservable = (function (_super) { | ||
__extends(ComputedObservable, _super); | ||
function ComputedObservable(func, scope) { | ||
_super.call(this, undefined); | ||
this.func = func; | ||
this.scope = scope; | ||
this.isComputing = false; | ||
this.hasError = false; | ||
if (typeof func !== "function") | ||
throw new Error("ComputedObservable requires a function"); | ||
} | ||
return this._value; | ||
}; | ||
ComputedObservable.prototype.set = function (_) { | ||
throw new Error(this.toString() + ": A computed observable does not accept new values!"); | ||
}; | ||
ComputedObservable.prototype.compute = function () { | ||
var newValue; | ||
try { | ||
ComputedObservable.prototype.get = function () { | ||
if (this.isComputing) | ||
throw new Error("Cycle detected"); | ||
this.isComputing = true; | ||
newValue = this.func.call(this.scope); | ||
this.hasError = false; | ||
var state = this.dependencyState; | ||
if (state.isSleeping) { | ||
if (DNode.trackingStack.length > 0) { | ||
state.wakeUp(); | ||
state.notifyObserved(); | ||
} | ||
else { | ||
this.compute(); | ||
} | ||
} | ||
else { | ||
state.notifyObserved(); | ||
} | ||
if (state.hasCycle) | ||
throw new Error("Cycle detected"); | ||
if (this.hasError) { | ||
if (mobservable.mobservableStatic.debugLevel) { | ||
console.trace(); | ||
warn(this + ": rethrowing caught exception to observer: " + this._value + (this._value.cause || '')); | ||
} | ||
throw this._value; | ||
} | ||
return this._value; | ||
}; | ||
ComputedObservable.prototype.set = function (_) { | ||
throw new Error(this.toString() + ": A computed observable does not accept new values!"); | ||
}; | ||
ComputedObservable.prototype.compute = function () { | ||
var newValue; | ||
try { | ||
if (this.isComputing) | ||
throw new Error("Cycle detected"); | ||
this.isComputing = true; | ||
newValue = this.func.call(this.scope); | ||
this.hasError = false; | ||
} | ||
catch (e) { | ||
this.hasError = true; | ||
console.error(this + "Caught error during computation: ", e); | ||
if (e instanceof Error) | ||
newValue = e; | ||
else { | ||
newValue = new Error("MobservableComputationError"); | ||
newValue.cause = e; | ||
} | ||
} | ||
this.isComputing = false; | ||
if (newValue !== this._value) { | ||
var oldValue = this._value; | ||
this._value = newValue; | ||
this.changeEvent.emit(newValue, oldValue); | ||
return true; | ||
} | ||
return false; | ||
}; | ||
ComputedObservable.prototype.toString = function () { | ||
return "ComputedObservable[" + this.func.toString() + "]"; | ||
}; | ||
return ComputedObservable; | ||
})(ObservableValue); | ||
var WatchedExpression = (function () { | ||
function WatchedExpression(expr, onInvalidate) { | ||
this.expr = expr; | ||
this.onInvalidate = onInvalidate; | ||
this.dependencyState = new DNode(this); | ||
this.didEvaluate = false; | ||
this.dependencyState.computeNextState(); | ||
} | ||
catch (e) { | ||
this.hasError = true; | ||
console.error(this + "Caught error during computation: ", e); | ||
if (e instanceof Error) | ||
newValue = e; | ||
WatchedExpression.prototype.compute = function () { | ||
if (!this.didEvaluate) { | ||
this.didEvaluate = true; | ||
this.value = this.expr(); | ||
} | ||
else { | ||
newValue = new Error("MobservableComputationError"); | ||
newValue.cause = e; | ||
this.dispose(); | ||
this.onInvalidate(); | ||
} | ||
return false; | ||
}; | ||
WatchedExpression.prototype.dispose = function () { | ||
this.dependencyState.dispose(); | ||
}; | ||
return WatchedExpression; | ||
})(); | ||
var DNodeState; | ||
(function (DNodeState) { | ||
DNodeState[DNodeState["STALE"] = 0] = "STALE"; | ||
DNodeState[DNodeState["PENDING"] = 1] = "PENDING"; | ||
DNodeState[DNodeState["READY"] = 2] = "READY"; | ||
})(DNodeState || (DNodeState = {})); | ||
; | ||
var DNode = (function () { | ||
function DNode(owner) { | ||
this.owner = owner; | ||
this.state = DNodeState.READY; | ||
this.isSleeping = true; | ||
this.hasCycle = false; | ||
this.observing = []; | ||
this.prevObserving = null; | ||
this.observers = []; | ||
this.dependencyChangeCount = 0; | ||
this.dependencyStaleCount = 0; | ||
this.isDisposed = false; | ||
this.externalRefenceCount = 0; | ||
this.isComputed = owner.compute !== undefined; | ||
} | ||
this.isComputing = false; | ||
if (newValue !== this._value) { | ||
var oldValue = this._value; | ||
this._value = newValue; | ||
this.changeEvent.emit(newValue, oldValue); | ||
return true; | ||
} | ||
return false; | ||
}; | ||
ComputedObservable.prototype.toString = function () { | ||
return "ComputedObservable[" + this.func.toString() + "]"; | ||
}; | ||
return ComputedObservable; | ||
})(ObservableValue); | ||
var DNodeState; | ||
(function (DNodeState) { | ||
DNodeState[DNodeState["STALE"] = 0] = "STALE"; | ||
DNodeState[DNodeState["PENDING"] = 1] = "PENDING"; | ||
DNodeState[DNodeState["READY"] = 2] = "READY"; | ||
})(DNodeState || (DNodeState = {})); | ||
; | ||
var DNode = (function () { | ||
function DNode(isComputed) { | ||
this.isComputed = isComputed; | ||
this.state = DNodeState.READY; | ||
this.isSleeping = true; | ||
this.hasCycle = false; | ||
this.observing = []; | ||
this.prevObserving = null; | ||
this.observers = []; | ||
this.dependencyChangeCount = 0; | ||
this.dependencyStaleCount = 0; | ||
this.isDisposed = false; | ||
this.externalRefenceCount = 0; | ||
} | ||
DNode.prototype.setRefCount = function (delta) { | ||
var rc = this.externalRefenceCount += delta; | ||
if (rc === 0) | ||
this.tryToSleep(); | ||
else if (rc === delta) | ||
this.wakeUp(); | ||
}; | ||
DNode.prototype.addObserver = function (node) { | ||
this.observers[this.observers.length] = node; | ||
}; | ||
DNode.prototype.removeObserver = function (node) { | ||
var obs = this.observers, idx = obs.indexOf(node); | ||
if (idx !== -1) { | ||
obs.splice(idx, 1); | ||
if (obs.length === 0) | ||
; | ||
DNode.prototype.setRefCount = function (delta) { | ||
var rc = this.externalRefenceCount += delta; | ||
if (rc === 0) | ||
this.tryToSleep(); | ||
} | ||
}; | ||
DNode.prototype.markStale = function () { | ||
if (this.state !== DNodeState.READY) | ||
return; | ||
this.state = DNodeState.STALE; | ||
this.notifyObservers(); | ||
}; | ||
DNode.prototype.markReady = function (stateDidActuallyChange) { | ||
if (this.state === DNodeState.READY) | ||
return; | ||
this.state = DNodeState.READY; | ||
this.notifyObservers(stateDidActuallyChange); | ||
if (this.observers.length === 0) | ||
Scheduler.scheduleReady(); | ||
}; | ||
DNode.prototype.notifyObservers = function (stateDidActuallyChange) { | ||
if (stateDidActuallyChange === void 0) { stateDidActuallyChange = false; } | ||
var os = this.observers.slice(); | ||
for (var l = os.length, i = 0; i < l; i++) | ||
os[i].notifyStateChange(this, stateDidActuallyChange); | ||
}; | ||
DNode.prototype.tryToSleep = function () { | ||
if (this.isComputed && this.observers.length === 0 && this.externalRefenceCount === 0 && !this.isSleeping) { | ||
for (var i = 0, l = this.observing.length; i < l; i++) | ||
this.observing[i].removeObserver(this); | ||
this.observing = []; | ||
this.isSleeping = true; | ||
} | ||
}; | ||
DNode.prototype.wakeUp = function () { | ||
if (this.isSleeping && this.isComputed) { | ||
this.isSleeping = false; | ||
this.state = DNodeState.PENDING; | ||
this.computeNextState(); | ||
} | ||
}; | ||
DNode.prototype.notifyStateChange = function (observable, stateDidActuallyChange) { | ||
var _this = this; | ||
if (observable.state === DNodeState.STALE) { | ||
if (++this.dependencyStaleCount === 1) | ||
this.markStale(); | ||
} | ||
else { | ||
if (stateDidActuallyChange) | ||
this.dependencyChangeCount += 1; | ||
if (--this.dependencyStaleCount === 0) { | ||
else if (rc === delta) | ||
this.wakeUp(); | ||
}; | ||
DNode.prototype.addObserver = function (node) { | ||
this.observers[this.observers.length] = node; | ||
}; | ||
DNode.prototype.removeObserver = function (node) { | ||
var obs = this.observers, idx = obs.indexOf(node); | ||
if (idx !== -1) { | ||
obs.splice(idx, 1); | ||
if (obs.length === 0) | ||
this.tryToSleep(); | ||
} | ||
}; | ||
DNode.prototype.markStale = function () { | ||
if (this.state !== DNodeState.READY) | ||
return; | ||
this.state = DNodeState.STALE; | ||
this.notifyObservers(); | ||
}; | ||
DNode.prototype.markReady = function (stateDidActuallyChange) { | ||
if (this.state === DNodeState.READY) | ||
return; | ||
this.state = DNodeState.READY; | ||
this.notifyObservers(stateDidActuallyChange); | ||
}; | ||
DNode.prototype.notifyObservers = function (stateDidActuallyChange) { | ||
if (stateDidActuallyChange === void 0) { stateDidActuallyChange = false; } | ||
var os = this.observers.slice(); | ||
for (var l = os.length, i = 0; i < l; i++) | ||
os[i].notifyStateChange(this, stateDidActuallyChange); | ||
}; | ||
DNode.prototype.tryToSleep = function () { | ||
if (!this.isSleeping && this.isComputed && this.observers.length === 0 && this.externalRefenceCount === 0) { | ||
for (var i = 0, l = this.observing.length; i < l; i++) | ||
this.observing[i].removeObserver(this); | ||
this.observing = []; | ||
this.isSleeping = true; | ||
} | ||
}; | ||
DNode.prototype.wakeUp = function () { | ||
if (this.isSleeping && this.isComputed) { | ||
this.isSleeping = false; | ||
this.state = DNodeState.PENDING; | ||
Scheduler.schedule(function () { | ||
if (_this.dependencyChangeCount > 0) | ||
_this.computeNextState(); | ||
else | ||
_this.markReady(false); | ||
_this.dependencyChangeCount = 0; | ||
}); | ||
this.computeNextState(); | ||
} | ||
} | ||
}; | ||
DNode.prototype.computeNextState = function () { | ||
this.trackDependencies(); | ||
var stateDidChange = this.nextState(); | ||
this.bindDependencies(); | ||
this.markReady(stateDidChange); | ||
}; | ||
DNode.prototype.nextState = function () { | ||
return false; | ||
}; | ||
DNode.prototype.trackDependencies = function () { | ||
this.prevObserving = this.observing; | ||
DNode.trackingStack[DNode.trackingStack.length] = []; | ||
}; | ||
DNode.prototype.bindDependencies = function () { | ||
this.observing = DNode.trackingStack.pop(); | ||
if (this.isComputed && this.observing.length === 0 && mobservableStatic.debugLevel > 1 && !this.isDisposed) { | ||
console.trace(); | ||
warn("You have created a function that doesn't observe any values, did you forget to make its dependencies observable?"); | ||
} | ||
var _a = quickDiff(this.observing, this.prevObserving), added = _a[0], removed = _a[1]; | ||
this.prevObserving = null; | ||
for (var i = 0, l = removed.length; i < l; i++) | ||
removed[i].removeObserver(this); | ||
this.hasCycle = false; | ||
for (var i = 0, l = added.length; i < l; i++) { | ||
if (this.isComputed && added[i].findCycle(this)) { | ||
this.hasCycle = true; | ||
this.observing.splice(this.observing.indexOf(added[i]), 1); | ||
added[i].hasCycle = true; | ||
}; | ||
DNode.prototype.notifyStateChange = function (observable, stateDidActuallyChange) { | ||
var _this = this; | ||
if (observable.state === DNodeState.STALE) { | ||
if (++this.dependencyStaleCount === 1) | ||
this.markStale(); | ||
} | ||
else { | ||
added[i].addObserver(this); | ||
if (stateDidActuallyChange) | ||
this.dependencyChangeCount += 1; | ||
if (--this.dependencyStaleCount === 0) { | ||
this.state = DNodeState.PENDING; | ||
Scheduler.schedule(function () { | ||
if (_this.dependencyChangeCount > 0) | ||
_this.computeNextState(); | ||
else | ||
_this.markReady(false); | ||
_this.dependencyChangeCount = 0; | ||
}); | ||
} | ||
} | ||
}; | ||
DNode.prototype.computeNextState = function () { | ||
this.trackDependencies(); | ||
var stateDidChange = this.owner.compute(); | ||
this.bindDependencies(); | ||
this.markReady(stateDidChange); | ||
}; | ||
DNode.prototype.trackDependencies = function () { | ||
this.prevObserving = this.observing; | ||
DNode.trackingStack[DNode.trackingStack.length] = []; | ||
}; | ||
DNode.prototype.bindDependencies = function () { | ||
this.observing = DNode.trackingStack.pop(); | ||
if (this.isComputed && this.observing.length === 0 && mobservable.mobservableStatic.debugLevel > 1 && !this.isDisposed) { | ||
console.trace(); | ||
warn("You have created a function that doesn't observe any values, did you forget to make its dependencies observable?"); | ||
} | ||
var _a = quickDiff(this.observing, this.prevObserving), added = _a[0], removed = _a[1]; | ||
this.prevObserving = null; | ||
for (var i = 0, l = removed.length; i < l; i++) | ||
removed[i].removeObserver(this); | ||
this.hasCycle = false; | ||
for (var i = 0, l = added.length; i < l; i++) { | ||
if (this.isComputed && added[i].findCycle(this)) { | ||
this.hasCycle = true; | ||
this.observing.splice(this.observing.indexOf(added[i]), 1); | ||
added[i].hasCycle = true; | ||
} | ||
else { | ||
added[i].addObserver(this); | ||
} | ||
} | ||
}; | ||
DNode.prototype.notifyObserved = function () { | ||
var ts = DNode.trackingStack, l = ts.length; | ||
if (l > 0) { | ||
var cs = ts[l - 1], csl = cs.length; | ||
if (cs[csl - 1] !== this && cs[csl - 2] !== this) | ||
cs[csl] = this; | ||
} | ||
}; | ||
DNode.prototype.findCycle = function (node) { | ||
var obs = this.observing; | ||
if (obs.indexOf(node) !== -1) | ||
return true; | ||
for (var l = obs.length, i = 0; i < l; i++) | ||
if (obs[i].findCycle(node)) | ||
return true; | ||
return false; | ||
}; | ||
DNode.prototype.dispose = function () { | ||
if (this.observers.length) | ||
throw new Error("Cannot dispose DNode; it is still being observed"); | ||
for (var l = this.observing.length, i = 0; i < l; i++) | ||
this.observing[i].removeObserver(this); | ||
this.observing = []; | ||
this.isDisposed = true; | ||
}; | ||
DNode.trackingStack = []; | ||
return DNode; | ||
})(); | ||
var StubArray = (function () { | ||
function StubArray() { | ||
} | ||
}; | ||
DNode.prototype.notifyObserved = function () { | ||
var ts = DNode.trackingStack, l = ts.length; | ||
if (l > 0) { | ||
var cs = ts[l - 1], csl = cs.length; | ||
if (cs[csl - 1] !== this && cs[csl - 2] !== this) | ||
cs[csl] = this; | ||
return StubArray; | ||
})(); | ||
StubArray.prototype = []; | ||
var ObservableArray = (function (_super) { | ||
__extends(ObservableArray, _super); | ||
function ObservableArray(initialValues) { | ||
_super.call(this); | ||
Object.defineProperties(this, { | ||
"dependencyState": { enumerable: false, value: new DNode(this) }, | ||
"_values": { enumerable: false, value: initialValues ? initialValues.slice() : [] }, | ||
"changeEvent": { enumerable: false, value: new SimpleEventEmitter() } | ||
}); | ||
if (initialValues && initialValues.length) | ||
this.updateLength(0, initialValues.length); | ||
} | ||
}; | ||
DNode.prototype.findCycle = function (node) { | ||
var obs = this.observing; | ||
if (obs.indexOf(node) !== -1) | ||
return true; | ||
for (var l = obs.length, i = 0; i < l; i++) | ||
if (obs[i].findCycle(node)) | ||
return true; | ||
return false; | ||
}; | ||
DNode.prototype.dispose = function () { | ||
if (this.observers.length) | ||
throw new Error("Cannot dispose DNode; it is still being observed"); | ||
for (var l = this.observing.length, i = 0; i < l; i++) | ||
this.observing[i].removeObserver(this); | ||
this.observing = []; | ||
this.isDisposed = true; | ||
}; | ||
DNode.trackingStack = []; | ||
return DNode; | ||
})(); | ||
var ObservableArray = (function () { | ||
function ObservableArray(initialValues) { | ||
Object.defineProperties(this, { | ||
"dependencyState": { enumerable: false, value: new DNode(false) }, | ||
"_values": { enumerable: false, value: initialValues ? initialValues.slice() : [] }, | ||
"changeEvent": { enumerable: false, value: new SimpleEventEmitter() }, | ||
Object.defineProperty(ObservableArray.prototype, "length", { | ||
get: function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.length; | ||
}, | ||
set: function (newLength) { | ||
if (typeof newLength !== "number" || newLength < 0) | ||
throw new Error("Out of range: " + newLength); | ||
var currentLength = this._values.length; | ||
if (newLength === currentLength) | ||
return; | ||
else if (newLength > currentLength) | ||
this.spliceWithArray(currentLength, 0, new Array(newLength - currentLength)); | ||
else | ||
this.spliceWithArray(newLength, currentLength - newLength); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
if (initialValues && initialValues.length) | ||
this.updateLength(0, initialValues.length); | ||
} | ||
Object.defineProperty(ObservableArray.prototype, "length", { | ||
get: function () { | ||
ObservableArray.prototype.updateLength = function (oldLength, delta) { | ||
if (delta < 0) | ||
for (var i = oldLength + delta; i < oldLength; i++) | ||
delete this[i]; | ||
else if (delta > 0) { | ||
if (oldLength + delta > ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE) | ||
ObservableArray.reserveArrayBuffer(oldLength + delta); | ||
for (var i = oldLength, end = oldLength + delta; i < end; i++) | ||
Object.defineProperty(this, "" + i, ObservableArray.ENUMERABLE_PROPS[i]); | ||
} | ||
}; | ||
ObservableArray.prototype.spliceWithArray = function (index, deleteCount, newItems) { | ||
var length = this._values.length; | ||
if ((newItems === undefined || newItems.length === 0) && (deleteCount === 0 || length === 0)) | ||
return []; | ||
if (index === undefined) | ||
index = 0; | ||
else if (index > length) | ||
index = length; | ||
else if (index < 0) | ||
index = Math.max(0, length + index); | ||
if (arguments.length === 1) | ||
deleteCount = length - index; | ||
else if (deleteCount === undefined || deleteCount === null) | ||
deleteCount = 0; | ||
else | ||
deleteCount = Math.max(0, Math.min(deleteCount, length - index)); | ||
if (newItems === undefined) | ||
newItems = []; | ||
var lengthDelta = newItems.length - deleteCount; | ||
var res = (_a = this._values).splice.apply(_a, [index, deleteCount].concat(newItems)); | ||
this.updateLength(length, lengthDelta); | ||
this.notifySplice(index, res, newItems); | ||
return res; | ||
var _a; | ||
}; | ||
ObservableArray.prototype.notifyChildUpdate = function (index, oldValue) { | ||
this.notifyChanged(); | ||
this.changeEvent.emit({ object: this, type: 'update', index: index, oldValue: oldValue }); | ||
}; | ||
ObservableArray.prototype.notifySplice = function (index, deleted, added) { | ||
if (deleted.length === 0 && added.length === 0) | ||
return; | ||
this.notifyChanged(); | ||
this.changeEvent.emit({ object: this, type: 'splice', index: index, addedCount: added.length, removed: deleted }); | ||
}; | ||
ObservableArray.prototype.notifyChanged = function () { | ||
this.dependencyState.markStale(); | ||
this.dependencyState.markReady(true); | ||
}; | ||
ObservableArray.prototype.observe = function (listener, fireImmediately) { | ||
if (fireImmediately === void 0) { fireImmediately = false; } | ||
if (fireImmediately) | ||
listener({ object: this, type: 'splice', index: 0, addedCount: this._values.length, removed: [] }); | ||
return this.changeEvent.on(listener); | ||
}; | ||
ObservableArray.prototype.clear = function () { | ||
return this.splice(0); | ||
}; | ||
ObservableArray.prototype.replace = function (newItems) { | ||
return this.spliceWithArray(0, this._values.length, newItems); | ||
}; | ||
ObservableArray.prototype.values = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.slice(); | ||
}; | ||
ObservableArray.prototype.toJSON = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.slice(); | ||
}; | ||
ObservableArray.prototype.clone = function () { | ||
this.dependencyState.notifyObserved(); | ||
return new ObservableArray(this._values); | ||
}; | ||
ObservableArray.prototype.find = function (predicate, thisArg, fromIndex) { | ||
if (fromIndex === void 0) { fromIndex = 0; } | ||
this.dependencyState.notifyObserved(); | ||
var items = this._values, l = items.length; | ||
for (var i = fromIndex; i < l; i++) | ||
if (predicate.call(thisArg, items[i], i, this)) | ||
return items[i]; | ||
return null; | ||
}; | ||
ObservableArray.prototype.splice = function (index, deleteCount) { | ||
var newItems = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
newItems[_i - 2] = arguments[_i]; | ||
} | ||
this.sideEffectWarning("splice"); | ||
switch (arguments.length) { | ||
case 0: | ||
return []; | ||
case 1: | ||
return this.spliceWithArray(index); | ||
case 2: | ||
return this.spliceWithArray(index, deleteCount); | ||
} | ||
return this.spliceWithArray(index, deleteCount, newItems); | ||
}; | ||
ObservableArray.prototype.push = function () { | ||
var items = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
items[_i - 0] = arguments[_i]; | ||
} | ||
this.sideEffectWarning("push"); | ||
this.spliceWithArray(this._values.length, 0, items); | ||
return this._values.length; | ||
}, | ||
set: function (newLength) { | ||
if (typeof newLength !== "number" || newLength < 0) | ||
throw new Error("Out of range: " + newLength); | ||
var currentLength = this._values.length; | ||
if (newLength === currentLength) | ||
return; | ||
else if (newLength > currentLength) | ||
this.spliceWithArray(currentLength, 0, new Array(newLength - currentLength)); | ||
else | ||
this.spliceWithArray(newLength, currentLength - newLength); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
ObservableArray.prototype.updateLength = function (oldLength, delta) { | ||
if (delta < 0) | ||
for (var i = oldLength + delta; i < oldLength; i++) | ||
delete this[i]; | ||
else if (delta > 0) { | ||
if (oldLength + delta > ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE) | ||
ObservableArray.reserveArrayBuffer(oldLength + delta); | ||
for (var i = oldLength, end = oldLength + delta; i < end; i++) | ||
Object.defineProperty(this, "" + i, ObservableArray.ENUMERABLE_PROPS[i]); | ||
}; | ||
ObservableArray.prototype.pop = function () { | ||
this.sideEffectWarning("pop"); | ||
return this.splice(Math.max(this._values.length - 1, 0), 1)[0]; | ||
}; | ||
ObservableArray.prototype.shift = function () { | ||
this.sideEffectWarning("shift"); | ||
return this.splice(0, 1)[0]; | ||
}; | ||
ObservableArray.prototype.unshift = function () { | ||
var items = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
items[_i - 0] = arguments[_i]; | ||
} | ||
this.sideEffectWarning("unshift"); | ||
this.spliceWithArray(0, 0, items); | ||
return this._values.length; | ||
}; | ||
ObservableArray.prototype.reverse = function () { | ||
this.sideEffectWarning("reverse"); | ||
return this.replace(this._values.reverse()); | ||
}; | ||
ObservableArray.prototype.sort = function (compareFn) { | ||
this.sideEffectWarning("sort"); | ||
return this.replace(this._values.sort.apply(this._values, arguments)); | ||
}; | ||
ObservableArray.prototype.remove = function (value) { | ||
this.sideEffectWarning("remove"); | ||
var idx = this._values.indexOf(value); | ||
if (idx > -1) { | ||
this.splice(idx, 1); | ||
return true; | ||
} | ||
return false; | ||
}; | ||
ObservableArray.prototype.toString = function () { return this.wrapReadFunction("toString", arguments); }; | ||
ObservableArray.prototype.toLocaleString = function () { return this.wrapReadFunction("toLocaleString", arguments); }; | ||
ObservableArray.prototype.concat = function () { return this.wrapReadFunction("concat", arguments); }; | ||
ObservableArray.prototype.join = function (separator) { return this.wrapReadFunction("join", arguments); }; | ||
ObservableArray.prototype.slice = function (start, end) { return this.wrapReadFunction("slice", arguments); }; | ||
ObservableArray.prototype.indexOf = function (searchElement, fromIndex) { return this.wrapReadFunction("indexOf", arguments); }; | ||
ObservableArray.prototype.lastIndexOf = function (searchElement, fromIndex) { return this.wrapReadFunction("lastIndexOf", arguments); }; | ||
ObservableArray.prototype.every = function (callbackfn, thisArg) { return this.wrapReadFunction("every", arguments); }; | ||
ObservableArray.prototype.some = function (callbackfn, thisArg) { return this.wrapReadFunction("some", arguments); }; | ||
ObservableArray.prototype.forEach = function (callbackfn, thisArg) { return this.wrapReadFunction("forEach", arguments); }; | ||
ObservableArray.prototype.map = function (callbackfn, thisArg) { return this.wrapReadFunction("map", arguments); }; | ||
ObservableArray.prototype.filter = function (callbackfn, thisArg) { return this.wrapReadFunction("filter", arguments); }; | ||
ObservableArray.prototype.reduce = function (callbackfn, initialValue) { return this.wrapReadFunction("reduce", arguments); }; | ||
ObservableArray.prototype.reduceRight = function (callbackfn, initialValue) { return this.wrapReadFunction("reduceRight", arguments); }; | ||
ObservableArray.prototype.wrapReadFunction = function (funcName, initialArgs) { | ||
var baseFunc = Array.prototype[funcName]; | ||
return (ObservableArray.prototype[funcName] = function () { | ||
this.dependencyState.notifyObserved(); | ||
return baseFunc.apply(this._values, arguments); | ||
}).apply(this, initialArgs); | ||
}; | ||
ObservableArray.prototype.sideEffectWarning = function (funcName) { | ||
if (DNode.trackingStack.length > 0) | ||
warn("[Mobservable.Array] The method array." + funcName + " should not be used inside observable functions since it has side-effects"); | ||
}; | ||
ObservableArray.createArrayBufferItem = function (index) { | ||
var prop = { | ||
enumerable: false, | ||
configurable: false, | ||
set: function (value) { | ||
if (index < this._values.length) { | ||
var oldValue = this._values[index]; | ||
if (oldValue !== value) { | ||
this._values[index] = value; | ||
this.notifyChildUpdate(index, oldValue); | ||
} | ||
} | ||
else if (index === this._values.length) | ||
this.push(value); | ||
else | ||
throw new Error("ObservableArray: Index out of bounds, " + index + " is larger than " + this.values.length); | ||
}, | ||
get: function () { | ||
if (index < this._values.length) { | ||
this.dependencyState.notifyObserved(); | ||
return this._values[index]; | ||
} | ||
return undefined; | ||
} | ||
}; | ||
Object.defineProperty(ObservableArray.prototype, "" + index, prop); | ||
prop.enumerable = true; | ||
prop.configurable = true; | ||
ObservableArray.ENUMERABLE_PROPS[index] = prop; | ||
}; | ||
ObservableArray.reserveArrayBuffer = function (max) { | ||
for (var index = ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE; index <= max; index++) | ||
ObservableArray.createArrayBufferItem(index); | ||
ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE = max; | ||
}; | ||
ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE = 0; | ||
ObservableArray.ENUMERABLE_PROPS = []; | ||
return ObservableArray; | ||
})(StubArray); | ||
ObservableArray.reserveArrayBuffer(1000); | ||
var SimpleEventEmitter = (function () { | ||
function SimpleEventEmitter() { | ||
this.listeners = []; | ||
} | ||
}; | ||
ObservableArray.prototype.spliceWithArray = function (index, deleteCount, newItems) { | ||
var length = this._values.length; | ||
if ((newItems === undefined || newItems.length === 0) && (deleteCount === 0 || length === 0)) | ||
return []; | ||
if (index === undefined) | ||
index = 0; | ||
else if (index > length) | ||
index = length; | ||
else if (index < 0) | ||
index = Math.max(0, length + index); | ||
if (arguments.length === 1) | ||
deleteCount = length - index; | ||
else if (deleteCount === undefined || deleteCount === null) | ||
deleteCount = 0; | ||
else | ||
deleteCount = Math.max(0, Math.min(deleteCount, length - index)); | ||
if (newItems === undefined) | ||
newItems = []; | ||
var lengthDelta = newItems.length - deleteCount; | ||
var res = (_a = this._values).splice.apply(_a, [index, deleteCount].concat(newItems)); | ||
this.updateLength(length, lengthDelta); | ||
this.notifySplice(index, res, newItems); | ||
return res; | ||
var _a; | ||
}; | ||
ObservableArray.prototype.notifyChildUpdate = function (index, oldValue) { | ||
this.notifyChanged(); | ||
this.changeEvent.emit({ object: this, type: 'update', index: index, oldValue: oldValue }); | ||
}; | ||
ObservableArray.prototype.notifySplice = function (index, deleted, added) { | ||
if (deleted.length === 0 && added.length === 0) | ||
return; | ||
this.notifyChanged(); | ||
this.changeEvent.emit({ object: this, type: 'splice', index: index, addedCount: added.length, removed: deleted }); | ||
}; | ||
ObservableArray.prototype.notifyChanged = function () { | ||
this.dependencyState.markStale(); | ||
this.dependencyState.markReady(true); | ||
}; | ||
ObservableArray.prototype.observe = function (listener, fireImmediately) { | ||
if (fireImmediately === void 0) { fireImmediately = false; } | ||
if (fireImmediately) | ||
listener({ object: this, type: 'splice', index: 0, addedCount: this._values.length, removed: [] }); | ||
return this.changeEvent.on(listener); | ||
}; | ||
ObservableArray.prototype.clear = function () { | ||
return this.splice(0); | ||
}; | ||
ObservableArray.prototype.replace = function (newItems) { | ||
return this.spliceWithArray(0, this._values.length, newItems); | ||
}; | ||
ObservableArray.prototype.values = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.slice(); | ||
}; | ||
ObservableArray.prototype.toJSON = function () { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.slice(); | ||
}; | ||
ObservableArray.prototype.clone = function () { | ||
this.dependencyState.notifyObserved(); | ||
return new ObservableArray(this._values); | ||
}; | ||
ObservableArray.prototype.splice = function (index, deleteCount) { | ||
var newItems = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
newItems[_i - 2] = arguments[_i]; | ||
SimpleEventEmitter.prototype.emit = function () { | ||
var listeners = this.listeners.slice(); | ||
var l = listeners.length; | ||
switch (arguments.length) { | ||
case 0: | ||
for (var i = 0; i < l; i++) | ||
listeners[i](); | ||
break; | ||
case 1: | ||
var data = arguments[0]; | ||
for (var i = 0; i < l; i++) | ||
listeners[i](data); | ||
break; | ||
default: | ||
for (var i = 0; i < l; i++) | ||
listeners[i].apply(null, arguments); | ||
} | ||
}; | ||
SimpleEventEmitter.prototype.on = function (listener) { | ||
var _this = this; | ||
this.listeners.push(listener); | ||
return once(function () { | ||
var idx = _this.listeners.indexOf(listener); | ||
if (idx !== -1) | ||
_this.listeners.splice(idx, 1); | ||
}); | ||
}; | ||
SimpleEventEmitter.prototype.once = function (listener) { | ||
var subscription = this.on(function () { | ||
subscription(); | ||
listener.apply(this, arguments); | ||
}); | ||
return subscription; | ||
}; | ||
return SimpleEventEmitter; | ||
})(); | ||
mobservable.mobservableStatic.SimpleEventEmitter = SimpleEventEmitter; | ||
var Scheduler = (function () { | ||
function Scheduler() { | ||
} | ||
switch (arguments.length) { | ||
case 0: | ||
return []; | ||
case 1: | ||
return this.spliceWithArray(index); | ||
case 2: | ||
return this.spliceWithArray(index, deleteCount); | ||
} | ||
return this.spliceWithArray(index, deleteCount, newItems); | ||
}; | ||
ObservableArray.prototype.push = function () { | ||
var items = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
items[_i - 0] = arguments[_i]; | ||
} | ||
this.spliceWithArray(this._values.length, 0, items); | ||
return this._values.length; | ||
}; | ||
ObservableArray.prototype.pop = function () { | ||
return this.splice(Math.max(this._values.length - 1, 0), 1)[0]; | ||
}; | ||
ObservableArray.prototype.shift = function () { | ||
return this.splice(0, 1)[0]; | ||
}; | ||
ObservableArray.prototype.unshift = function () { | ||
var items = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
items[_i - 0] = arguments[_i]; | ||
} | ||
this.spliceWithArray(0, 0, items); | ||
return this._values.length; | ||
}; | ||
ObservableArray.prototype.reverse = function () { | ||
return this.replace(this._values.reverse()); | ||
}; | ||
ObservableArray.prototype.sort = function (compareFn) { | ||
return this.replace(this._values.sort.apply(this._values, arguments)); | ||
}; | ||
ObservableArray.prototype.toString = function () { return this.wrapReadFunction("toString", arguments); }; | ||
ObservableArray.prototype.toLocaleString = function () { return this.wrapReadFunction("toLocaleString", arguments); }; | ||
ObservableArray.prototype.concat = function () { return this.wrapReadFunction("concat", arguments); }; | ||
ObservableArray.prototype.join = function (separator) { return this.wrapReadFunction("join", arguments); }; | ||
ObservableArray.prototype.slice = function (start, end) { return this.wrapReadFunction("slice", arguments); }; | ||
ObservableArray.prototype.indexOf = function (searchElement, fromIndex) { return this.wrapReadFunction("indexOf", arguments); }; | ||
ObservableArray.prototype.lastIndexOf = function (searchElement, fromIndex) { return this.wrapReadFunction("lastIndexOf", arguments); }; | ||
ObservableArray.prototype.every = function (callbackfn, thisArg) { return this.wrapReadFunction("every", arguments); }; | ||
ObservableArray.prototype.some = function (callbackfn, thisArg) { return this.wrapReadFunction("some", arguments); }; | ||
ObservableArray.prototype.forEach = function (callbackfn, thisArg) { return this.wrapReadFunction("forEach", arguments); }; | ||
ObservableArray.prototype.map = function (callbackfn, thisArg) { return this.wrapReadFunction("map", arguments); }; | ||
ObservableArray.prototype.filter = function (callbackfn, thisArg) { return this.wrapReadFunction("filter", arguments); }; | ||
ObservableArray.prototype.reduce = function (callbackfn, initialValue) { return this.wrapReadFunction("reduce", arguments); }; | ||
ObservableArray.prototype.reduceRight = function (callbackfn, initialValue) { return this.wrapReadFunction("reduceRight", arguments); }; | ||
ObservableArray.prototype.wrapReadFunction = function (funcName, initialArgs) { | ||
var baseFunc = Array.prototype[funcName]; | ||
return (ObservableArray.prototype[funcName] = function () { | ||
this.dependencyState.notifyObserved(); | ||
return baseFunc.apply(this._values, arguments); | ||
}).apply(this, initialArgs); | ||
}; | ||
ObservableArray.createArrayBufferItem = function (index) { | ||
var prop = { | ||
enumerable: false, | ||
configurable: true, | ||
set: function (value) { | ||
if (index < this._values.length) { | ||
var oldValue = this._values[index]; | ||
if (oldValue !== value) { | ||
this._values[index] = value; | ||
this.notifyChildUpdate(index, oldValue); | ||
} | ||
Scheduler.schedule = function (func) { | ||
if (Scheduler.inBatch < 1) | ||
func(); | ||
else | ||
Scheduler.tasks[Scheduler.tasks.length] = func; | ||
}; | ||
Scheduler.runPostBatchActions = function () { | ||
var i = 0; | ||
while (Scheduler.tasks.length) { | ||
try { | ||
for (; i < Scheduler.tasks.length; i++) | ||
Scheduler.tasks[i](); | ||
Scheduler.tasks = []; | ||
} | ||
else if (index === this._values.length) | ||
this.push(value); | ||
else | ||
throw new Error("ObservableArray: Index out of bounds, " + index + " is larger than " + this.values.length); | ||
}, | ||
get: function () { | ||
if (index < this._values.length) { | ||
this.dependencyState.notifyObserved(); | ||
return this._values[index]; | ||
catch (e) { | ||
console.error("Failed to run scheduled action, the action has been dropped from the queue: " + e, e); | ||
Scheduler.tasks.splice(0, i + 1); | ||
} | ||
return undefined; | ||
} | ||
}; | ||
Object.defineProperty(ObservableArray.prototype, "" + index, prop); | ||
prop.enumerable = true; | ||
ObservableArray.ENUMERABLE_PROPS[index] = prop; | ||
}; | ||
ObservableArray.reserveArrayBuffer = function (max) { | ||
for (var index = ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE; index <= max; index++) | ||
ObservableArray.createArrayBufferItem(index); | ||
ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE = max; | ||
}; | ||
ObservableArray.OBSERVABLE_ARRAY_BUFFER_SIZE = 0; | ||
ObservableArray.ENUMERABLE_PROPS = []; | ||
return ObservableArray; | ||
})(); | ||
ObservableArray.reserveArrayBuffer(1000); | ||
var SimpleEventEmitter = (function () { | ||
function SimpleEventEmitter() { | ||
this.listeners = []; | ||
} | ||
SimpleEventEmitter.prototype.emit = function () { | ||
var listeners = this.listeners.slice(); | ||
var l = listeners.length; | ||
switch (arguments.length) { | ||
case 0: | ||
for (var i = 0; i < l; i++) | ||
listeners[i](); | ||
break; | ||
case 1: | ||
var data = arguments[0]; | ||
for (var i = 0; i < l; i++) | ||
listeners[i](data); | ||
break; | ||
default: | ||
for (var i = 0; i < l; i++) | ||
listeners[i].apply(null, arguments); | ||
} | ||
}; | ||
SimpleEventEmitter.prototype.on = function (listener) { | ||
var _this = this; | ||
this.listeners.push(listener); | ||
return once(function () { | ||
var idx = _this.listeners.indexOf(listener); | ||
if (idx !== -1) | ||
_this.listeners.splice(idx, 1); | ||
}); | ||
}; | ||
SimpleEventEmitter.prototype.once = function (listener) { | ||
var subscription = this.on(function () { | ||
subscription(); | ||
listener.apply(this, arguments); | ||
}); | ||
return subscription; | ||
}; | ||
return SimpleEventEmitter; | ||
})(); | ||
mobservableStatic.SimpleEventEmitter = SimpleEventEmitter; | ||
var Scheduler = (function () { | ||
function Scheduler() { | ||
} | ||
Scheduler.schedule = function (func) { | ||
if (Scheduler.inBatch < 1) | ||
func(); | ||
else | ||
Scheduler.tasks[Scheduler.tasks.length] = func; | ||
}; | ||
Scheduler.runPostBatchActions = function () { | ||
var i = 0; | ||
try { | ||
for (; i < Scheduler.tasks.length; i++) | ||
Scheduler.tasks[i](); | ||
Scheduler.tasks = []; | ||
} | ||
catch (e) { | ||
console.error("Failed to run scheduled action, the action has been dropped from the queue: " + e, e); | ||
Scheduler.tasks.splice(0, i + 1); | ||
setTimeout(Scheduler.runPostBatchActions, 1); | ||
throw e; | ||
} | ||
}; | ||
Scheduler.batch = function (action) { | ||
Scheduler.inBatch += 1; | ||
try { | ||
return action(); | ||
} | ||
finally { | ||
if (--Scheduler.inBatch === 0) { | ||
Scheduler.runPostBatchActions(); | ||
Scheduler.scheduleReady(); | ||
Scheduler.batch = function (action) { | ||
Scheduler.inBatch += 1; | ||
try { | ||
return action(); | ||
} | ||
} | ||
}; | ||
Scheduler.scheduleReady = function () { | ||
if (!Scheduler.pendingReady) { | ||
Scheduler.pendingReady = true; | ||
setTimeout(function () { | ||
Scheduler.pendingReady = false; | ||
Scheduler.readyEvent.emit(); | ||
}, 1); | ||
} | ||
}; | ||
Scheduler.onReady = function (listener) { | ||
return Scheduler.readyEvent.on(listener); | ||
}; | ||
Scheduler.onceReady = function (listener) { | ||
return Scheduler.readyEvent.once(listener); | ||
}; | ||
Scheduler.pendingReady = false; | ||
Scheduler.readyEvent = new SimpleEventEmitter(); | ||
Scheduler.inBatch = 0; | ||
Scheduler.tasks = []; | ||
return Scheduler; | ||
})(); | ||
function quickDiff(current, base) { | ||
if (!base.length) | ||
return [current, []]; | ||
if (!current.length) | ||
return [[], base]; | ||
var added = []; | ||
var removed = []; | ||
var currentIndex = 0, currentSearch = 0, currentLength = current.length, currentExhausted = false, baseIndex = 0, baseSearch = 0, baseLength = base.length, isSearching = false, baseExhausted = false; | ||
while (!baseExhausted && !currentExhausted) { | ||
if (!isSearching) { | ||
if (currentIndex < currentLength && baseIndex < baseLength && current[currentIndex] === base[baseIndex]) { | ||
currentIndex++; | ||
finally { | ||
if (--Scheduler.inBatch === 0) { | ||
Scheduler.inBatch += 1; | ||
Scheduler.runPostBatchActions(); | ||
Scheduler.inBatch -= 1; | ||
} | ||
} | ||
}; | ||
Scheduler.inBatch = 0; | ||
Scheduler.tasks = []; | ||
return Scheduler; | ||
})(); | ||
function quickDiff(current, base) { | ||
if (!base.length) | ||
return [current, []]; | ||
if (!current.length) | ||
return [[], base]; | ||
var added = []; | ||
var removed = []; | ||
var currentIndex = 0, currentSearch = 0, currentLength = current.length, currentExhausted = false, baseIndex = 0, baseSearch = 0, baseLength = base.length, isSearching = false, baseExhausted = false; | ||
while (!baseExhausted && !currentExhausted) { | ||
if (!isSearching) { | ||
if (currentIndex < currentLength && baseIndex < baseLength && current[currentIndex] === base[baseIndex]) { | ||
currentIndex++; | ||
baseIndex++; | ||
if (currentIndex === currentLength && baseIndex === baseLength) | ||
return [added, removed]; | ||
continue; | ||
} | ||
currentSearch = currentIndex; | ||
baseSearch = baseIndex; | ||
isSearching = true; | ||
} | ||
baseSearch += 1; | ||
currentSearch += 1; | ||
if (baseSearch >= baseLength) | ||
baseExhausted = true; | ||
if (currentSearch >= currentLength) | ||
currentExhausted = true; | ||
if (!currentExhausted && current[currentSearch] === base[baseIndex]) { | ||
added.push.apply(added, current.slice(currentIndex, currentSearch)); | ||
currentIndex = currentSearch + 1; | ||
baseIndex++; | ||
if (currentIndex === currentLength && baseIndex === baseLength) | ||
return [added, removed]; | ||
continue; | ||
isSearching = false; | ||
} | ||
currentSearch = currentIndex; | ||
baseSearch = baseIndex; | ||
isSearching = true; | ||
else if (!baseExhausted && base[baseSearch] === current[currentIndex]) { | ||
removed.push.apply(removed, base.slice(baseIndex, baseSearch)); | ||
baseIndex = baseSearch + 1; | ||
currentIndex++; | ||
isSearching = false; | ||
} | ||
} | ||
baseSearch += 1; | ||
currentSearch += 1; | ||
if (baseSearch >= baseLength) | ||
baseExhausted = true; | ||
if (currentSearch >= currentLength) | ||
currentExhausted = true; | ||
if (!currentExhausted && current[currentSearch] === base[baseIndex]) { | ||
added.push.apply(added, current.slice(currentIndex, currentSearch)); | ||
currentIndex = currentSearch + 1; | ||
baseIndex++; | ||
isSearching = false; | ||
} | ||
else if (!baseExhausted && base[baseSearch] === current[currentIndex]) { | ||
removed.push.apply(removed, base.slice(baseIndex, baseSearch)); | ||
baseIndex = baseSearch + 1; | ||
currentIndex++; | ||
isSearching = false; | ||
} | ||
added.push.apply(added, current.slice(currentIndex)); | ||
removed.push.apply(removed, base.slice(baseIndex)); | ||
return [added, removed]; | ||
} | ||
added.push.apply(added, current.slice(currentIndex)); | ||
removed.push.apply(removed, base.slice(baseIndex)); | ||
return [added, removed]; | ||
} | ||
mobservableStatic.quickDiff = quickDiff; | ||
mobservableStatic.stackDepth = function () { return DNode.trackingStack.length; }; | ||
function warn(message) { | ||
if (console) | ||
console.warn("[WARNING:mobservable] " + message); | ||
} | ||
function once(func) { | ||
var invoked = false; | ||
return function () { | ||
if (invoked) | ||
return; | ||
invoked = true; | ||
return func.apply(this, arguments); | ||
}; | ||
} | ||
module.exports = mobservableStatic; | ||
//# sourceMappingURL=mobservable.js.map | ||
mobservable.mobservableStatic.quickDiff = quickDiff; | ||
mobservable.mobservableStatic.stackDepth = function () { return DNode.trackingStack.length; }; | ||
function warn(message) { | ||
if (console) | ||
console.warn("[WARNING:mobservable] " + message); | ||
} | ||
function once(func) { | ||
var invoked = false; | ||
return function () { | ||
if (invoked) | ||
return; | ||
invoked = true; | ||
return func.apply(this, arguments); | ||
}; | ||
} | ||
})(mobservable || (mobservable = {})); | ||
(function (root, factory) { | ||
if (typeof define === 'function' && define.amd) { | ||
define('mobservable', [], function () { | ||
return (factory()); | ||
}); | ||
} | ||
else if (typeof exports === 'object') { | ||
module.exports = factory(); | ||
} | ||
else { | ||
root['mobservable'] = factory(); | ||
} | ||
}(this, function () { | ||
return mobservable.mobservableStatic; | ||
})); |
@@ -0,36 +1,26 @@ | ||
var fs = require('fs'); | ||
var mkdirp = require('mkdirp'); | ||
var path = require('path'); | ||
module.exports = function(grunt) { | ||
var tsc = "node " + __dirname + "/node_modules/typescript/bin/tsc.js".replace(/\//g, path.sep); | ||
console.log("Compiling with: " + tsc); | ||
grunt.initConfig({ | ||
ts: { | ||
options: { | ||
module: 'commonjs', | ||
target: 'es5' | ||
}, | ||
builddist : { | ||
src: ["mobservable.ts"], | ||
outDir: "dist/", | ||
comments: false, | ||
}, | ||
buildlocal : { | ||
src: ["mobservable.ts"] | ||
}, | ||
buildtypescripttest: { | ||
options: { | ||
compiler: './node_modules/typescript/bin/tsc' | ||
}, | ||
src: ["test/typescript-test.ts"] | ||
} | ||
}, | ||
nodeunit: { | ||
options: { | ||
reporter: 'default' | ||
}, | ||
options: { reporter: 'default' }, | ||
all: ['test/*.js'], | ||
perf: ['test/performance.js'] | ||
perf: ['test/perf/*.js'] | ||
}, | ||
exec: { | ||
cover: "mkdir -p dist/test && cp -rf test/* dist/test && istanbul cover nodeunit dist/test/" | ||
cover: "istanbul cover nodeunit test/", | ||
buildtypescripttest: { | ||
cmd: tsc + " typescript-test.ts -m commonjs -t es5", | ||
cwd: "test/" | ||
}, | ||
buildlocal: tsc + " mobservable.ts -t es5 --sourceMap", | ||
builddist: tsc + " mobservable.ts -t es5 --removeComments -out dist/mobservable.js" | ||
}, | ||
coveralls: { | ||
options: { | ||
// LCOV coverage file relevant to every target | ||
// LCOV coverage file relevant to every target | ||
force: false | ||
@@ -41,18 +31,41 @@ }, | ||
} | ||
} | ||
}, | ||
uglify: { | ||
dist: { | ||
files: { | ||
'dist/mobservable.min.js': ['dist/mobservable.js'] | ||
} | ||
} | ||
} | ||
}); | ||
grunt.loadNpmTasks("grunt-ts"); | ||
grunt.loadNpmTasks('grunt-contrib-nodeunit'); | ||
grunt.loadNpmTasks('grunt-contrib-uglify'); | ||
grunt.loadNpmTasks('grunt-coveralls'); | ||
grunt.loadNpmTasks('grunt-exec'); | ||
grunt.registerTask("buildDts", "Build .d.ts file", function() { | ||
var moduleDeclaration = '\n\ndeclare module "mobservable" {\n\tvar m : IMObservableStatic;\n\texport = m;\n}'; | ||
var ts = fs.readFileSync('mobservable.ts','utf8'); | ||
var headerEndIndex = ts.indexOf("/* END OF DECLARATION */"); | ||
if (headerEndIndex === -1) | ||
throw "Failed to find end of declaration in mobservable.ts"; | ||
fs.writeFileSync('mobservable.d.ts', "/** GENERATED FILE */\n" + ts.substr(0, headerEndIndex) + moduleDeclaration, 'utf8'); | ||
}); | ||
grunt.registerTask("preparetest", "Create node module in test folder", function(sourceDir) { | ||
mkdirp.sync("test/node_modules/mobservable"); | ||
fs.writeFileSync("test/node_modules/mobservable/mobservable.d.ts", fs.readFileSync("mobservable.d.ts","utf8"),"utf8"); | ||
fs.writeFileSync("test/node_modules/mobservable/index.js", "module.exports=require('../../../" + sourceDir + "/mobservable.js');","utf8"); | ||
}); | ||
grunt.registerTask("publish", "Publish to npm", function() { | ||
require("./publish.js"); | ||
}); | ||
grunt.registerTask("default", ["ts:buildlocal"]); | ||
grunt.registerTask("build", ["ts:builddist"]); | ||
grunt.registerTask("cover", ["ts:buildlocal", "exec:cover", "coveralls:default"]); | ||
grunt.registerTask("test", ["ts:buildlocal","ts:buildtypescripttest", "nodeunit:all"]); | ||
grunt.registerTask("perf", ["ts:buildlocal", "nodeunit:perf"]); | ||
grunt.registerTask("default", ["buildlocal"]); | ||
grunt.registerTask("builddist", ["exec:builddist","buildDts","uglify:dist"]); | ||
grunt.registerTask("buildlocal", ["exec:buildlocal", "buildDts"]); | ||
grunt.registerTask("cover", ["builddist", "preparetest:dist", "exec:cover", "coveralls:default"]); | ||
grunt.registerTask("test", ["buildlocal", "preparetest:", "exec:buildtypescripttest", "nodeunit:all"]); | ||
grunt.registerTask("perf", ["buildlocal", "preparetest:", "nodeunit:perf"]); | ||
}; |
@@ -1,61 +0,92 @@ | ||
declare module "mobservable" { | ||
interface Lambda { | ||
(): void; | ||
} | ||
interface IObservableValue<T, S> { | ||
(): T; | ||
(value: T): S; | ||
observe(callback: (newValue: T, oldValue: T) => void, fireImmediately:boolean): Lambda; | ||
} | ||
/** GENERATED FILE */ | ||
/** | ||
* MOBservable | ||
* (c) 2015 - Michel Weststrate | ||
* https://github.com/mweststrate/mobservable | ||
*/ | ||
interface IMObservableStatic { | ||
// ways of creating observables. | ||
<T>(value?:T[]):IObservableArray<T>; | ||
<T>(value?:T|{():T}, scope?:Object):IObservableValue<T>; | ||
value<T>(value?:T[]):IObservableArray<T>; | ||
value<T>(value?:T|{():T}, scope?:Object):IObservableValue<T>; | ||
array<T>(values?:T[]):IObservableArray<T>; | ||
primitive<T>(value?:T):IObservableValue<T>; | ||
reference<T>(value?:T):IObservableValue<T>; | ||
computed<T>(value:()=>T,scope?):IObservableValue<T>; | ||
export function array<T>(values?:T[]): IObservableArray<T>; | ||
export function value<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
export function watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
export function observeProperty(object:Object, key:string, listener:Function, invokeImmediately?:boolean):Lambda; | ||
// create observable properties | ||
props(object:Object, name:string, initalValue: any); | ||
props(object:Object, props:Object); | ||
props(object:Object); | ||
observable(target:Object, key:string); // annotation | ||
export function toJSON<T>(any:T):T; | ||
// annotation | ||
export function observable(target:Object, key:string); | ||
// observables to not observables | ||
toPlainValue<T>(any:T):T; | ||
export function batch<T>(action:()=>T):T; | ||
export function onReady(listener:Lambda):Lambda; | ||
export function onceReady(listener:Lambda); | ||
export function defineObservableProperty<T>(object:Object, name:string, initialValue?:T); | ||
export function initializeObservableProperties(object:Object); | ||
// observe observables | ||
observeProperty(object:Object, key:string, listener:Function, invokeImmediately?:boolean):Lambda; | ||
watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
// change a lot of observables at once | ||
batch<T>(action:()=>T):T; | ||
export var SimpleEventEmitter: new() => ISimpleEventEmitter; | ||
// Utils | ||
debugLevel: number; | ||
SimpleEventEmitter: new()=> ISimpleEventEmitter; | ||
} | ||
interface IObservableArray<T> extends Array<T> { | ||
[n: number]: T; | ||
length: number; | ||
interface Lambda { | ||
():void; | ||
} | ||
spliceWithArray(index:number, deleteCount?:number, newItems?:T[]):T[]; | ||
observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda; | ||
clear(): T[]; | ||
replace(newItems:T[]); | ||
values(): T[]; | ||
clone(): IObservableArray<T>; | ||
} | ||
interface IArrayChange<T> { | ||
type: string; // Always: 'update' | ||
object: IObservableArray<T>; | ||
index: number; | ||
oldValue: T; | ||
} | ||
interface IArraySplice<T> { | ||
type: string; // Always: 'splice' | ||
object: IObservableArray<T>; | ||
index: number; | ||
removed: T[]; | ||
addedCount: number; | ||
} | ||
interface IObservable { | ||
observe(callback:(...args:any[])=>void, fireImmediately?:boolean):Lambda; | ||
} | ||
interface ISimpleEventEmitter { | ||
emit(...data:any[]):void; | ||
on(listener:(...data:any[])=>void):Lambda; | ||
once(listener:(...data:any[])=>void):Lambda; | ||
} | ||
interface IObservableValue<T> extends IObservable { | ||
():T; | ||
(value:T); | ||
observe(callback:(newValue:T, oldValue:T)=>void, fireImmediately?:boolean):Lambda; | ||
} | ||
interface IObservableArray<T> extends IObservable, Array<T> { | ||
spliceWithArray(index:number, deleteCount?:number, newItems?:T[]):T[]; | ||
observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda; | ||
clear(): T[]; | ||
replace(newItems:T[]); | ||
values(): T[]; | ||
clone(): IObservableArray<T>; | ||
find(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T; | ||
remove(value:T):boolean; | ||
} | ||
interface IArrayChange<T> { | ||
type: string; // Always: 'update' | ||
object: IObservableArray<T>; | ||
index: number; | ||
oldValue: T; | ||
} | ||
interface IArraySplice<T> { | ||
type: string; // Always: 'splice' | ||
object: IObservableArray<T>; | ||
index: number; | ||
removed: T[]; | ||
addedCount: number; | ||
} | ||
interface ISimpleEventEmitter { | ||
emit(...data:any[]):void; | ||
on(listener:(...data:any[])=>void):Lambda; | ||
once(listener:(...data:any[])=>void):Lambda; | ||
} | ||
declare module "mobservable" { | ||
var m : IMObservableStatic; | ||
export = m; | ||
} |
@@ -6,3 +6,36 @@ /** | ||
*/ | ||
interface IMObservableStatic { | ||
// ways of creating observables. | ||
<T>(value?:T[]):IObservableArray<T>; | ||
<T>(value?:T|{():T}, scope?:Object):IObservableValue<T>; | ||
value<T>(value?:T[]):IObservableArray<T>; | ||
value<T>(value?:T|{():T}, scope?:Object):IObservableValue<T>; | ||
array<T>(values?:T[]):IObservableArray<T>; | ||
primitive<T>(value?:T):IObservableValue<T>; | ||
reference<T>(value?:T):IObservableValue<T>; | ||
computed<T>(value:()=>T,scope?):IObservableValue<T>; | ||
// create observable properties | ||
props(object:Object, name:string, initalValue: any); | ||
props(object:Object, props:Object); | ||
props(object:Object); | ||
observable(target:Object, key:string); // annotation | ||
// observables to not observables | ||
toPlainValue<T>(any:T):T; | ||
// observe observables | ||
observeProperty(object:Object, key:string, listener:Function, invokeImmediately?:boolean):Lambda; | ||
watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
// change a lot of observables at once | ||
batch<T>(action:()=>T):T; | ||
// Utils | ||
debugLevel: number; | ||
SimpleEventEmitter: new()=> ISimpleEventEmitter; | ||
} | ||
interface Lambda { | ||
@@ -12,102 +45,171 @@ ():void; | ||
interface IObservableValue<T,S> { | ||
interface IObservable { | ||
observe(callback:(...args:any[])=>void, fireImmediately?:boolean):Lambda; | ||
} | ||
interface IObservableValue<T> extends IObservable { | ||
():T; | ||
(value:T):S; | ||
(value:T); | ||
observe(callback:(newValue:T, oldValue:T)=>void, fireImmediately?:boolean):Lambda; | ||
} | ||
interface MobservableStatic { | ||
// shorthand for .value() | ||
<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
interface IObservableArray<T> extends IObservable, Array<T> { | ||
spliceWithArray(index:number, deleteCount?:number, newItems?:T[]):T[]; | ||
observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda; | ||
clear(): T[]; | ||
replace(newItems:T[]); | ||
values(): T[]; | ||
clone(): IObservableArray<T>; | ||
find(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T; | ||
remove(value:T):boolean; | ||
} | ||
// core functinos | ||
array<T>(values?:T[]): ObservableArray<T>; | ||
value<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
toJSON<T>(any:T):T; | ||
// property definition | ||
observable(target:Object, key:string); // annotation | ||
defineObservableProperty<T>(object:Object, name:string, initialValue?:T); | ||
initializeObservableProperties(object:Object); | ||
observeProperty(object:Object, key:string, listener:Function, invokeImmediately?:boolean):Lambda; | ||
interface IArrayChange<T> { | ||
type: string; // Always: 'update' | ||
object: IObservableArray<T>; | ||
index: number; | ||
oldValue: T; | ||
} | ||
// batching | ||
batch<T>(action:()=>T):T; | ||
onReady(listener:Lambda):Lambda; | ||
onceReady(listener:Lambda); | ||
interface IArraySplice<T> { | ||
type: string; // Always: 'splice' | ||
object: IObservableArray<T>; | ||
index: number; | ||
removed: T[]; | ||
addedCount: number; | ||
} | ||
// Utils | ||
SimpleEventEmitter: new()=> SimpleEventEmitter; | ||
debugLevel: number; | ||
interface ISimpleEventEmitter { | ||
emit(...data:any[]):void; | ||
on(listener:(...data:any[])=>void):Lambda; | ||
once(listener:(...data:any[])=>void):Lambda; | ||
} | ||
/** | ||
Creates an observable from either a value or a function. | ||
If a scope is provided, the function will be always executed usign the provided scope. | ||
Returns a new IObservable, that is, a functon that can be used to set a new value or get the current values | ||
(the latter if no arguments are provided) | ||
*/ | ||
function createObservable<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S> { | ||
var prop:ObservableValue<T,S> = null; | ||
/* END OF DECLARATION */ | ||
if (Array.isArray && Array.isArray(value) && mobservableStatic.debugLevel) | ||
warn("mobservable.value() was invoked with an array. Probably you want to create an mobservable.array() instead of observing a reference to an array?"); | ||
module mobservable { // wrap in module for UMD export, see end of the file | ||
function createObservable<T>(value:T[]):IObservableArray<T>; | ||
function createObservable<T>(value?:T|{():T}, scope?:Object):IObservableValue<T>; | ||
function createObservable(value?, scope?:Object):any { | ||
if (Array.isArray(value)) | ||
return new ObservableArray(value); | ||
if (typeof value === "function") | ||
prop = new ComputedObservable(<()=>T>value, scope); | ||
else | ||
prop = new ObservableValue(<T>value, scope); | ||
var propFunc = function(value?:T):T|S { | ||
if (arguments.length > 0) | ||
return <S> prop.set(value); | ||
else | ||
return <T> prop.get(); | ||
}; | ||
(<any>propFunc).observe = prop.observe.bind(prop); | ||
(<any>propFunc).prop = prop; | ||
(<any>propFunc).toString = function() { return prop.toString(); }; | ||
return <IObservableValue<T,S>> propFunc; | ||
return mobservableStatic.computed(value, scope); | ||
return mobservableStatic.primitive(value); | ||
} | ||
/** | ||
@see mobservableStatic.value | ||
*/ | ||
var mobservableStatic:MobservableStatic = <MobservableStatic> function<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S> { | ||
export var mobservableStatic:IMObservableStatic = <IMObservableStatic> function(value, scope?) { | ||
return createObservable(value,scope); | ||
}; | ||
/** | ||
@see createObservable | ||
*/ | ||
mobservableStatic.value = createObservable; | ||
mobservableStatic.primitive = mobservableStatic.reference = function(value?) { | ||
return new ObservableValue(value).createGetterSetter(); | ||
} | ||
mobservableStatic.computed = function<T>(func:()=>void, scope?) { | ||
return new ComputedObservable(func, scope).createGetterSetter(); | ||
} | ||
mobservableStatic.array = function array<T>(values?:T[]): ObservableArray<T> { | ||
return new ObservableArray(values); | ||
} | ||
mobservableStatic.props = function props(target, props?, value?) { | ||
switch(arguments.length) { | ||
case 0: | ||
throw new Error("Not enough arguments"); | ||
case 1: | ||
return mobservableStatic.props(target, target); // mix target properties into itself | ||
case 2: | ||
for(var key in props) | ||
mobservableStatic.props(target, key, props[key]); | ||
break; | ||
case 3: | ||
var isArray = Array.isArray(value); | ||
var observable = mobservableStatic.value(value, target); | ||
Object.defineProperty(target, props, { | ||
get: isArray | ||
? function() { return observable; } | ||
: observable, | ||
set: isArray | ||
? function(newValue) { (<IObservableArray<any>><any>observable).replace(newValue) } | ||
: observable, | ||
enumerable: true, | ||
configurable: false | ||
}); | ||
break; | ||
} | ||
return target; | ||
} | ||
/** | ||
DebugLevel: level 0: warnings only, level 1 or higher, prints a lot of messages. | ||
*/ | ||
mobservableStatic.debugLevel = 0; | ||
* Use this annotation to wrap properties of an object in an observable, for example: | ||
* class OrderLine { | ||
* @observable amount = 3; | ||
* @observable price = 2; | ||
* @observable total() { | ||
* return this.amount * this.price; | ||
* } | ||
* } | ||
*/ | ||
mobservableStatic.observable = function observable(target:Object, key:string, descriptor?) { | ||
var baseValue = descriptor ? descriptor.value : null; | ||
// observable annotations are invoked on the prototype, not on actual instances, | ||
// so upon invocation, determine the 'this' instance, and define a property on the | ||
// instance as well (that hides the propotype property) | ||
if (typeof baseValue === "function") { | ||
delete descriptor.value; | ||
delete descriptor.writable; | ||
descriptor.get = function() { | ||
mobservableStatic.props(this, key, baseValue); | ||
return this[key]; | ||
} | ||
descriptor.set = function () { | ||
console.trace(); | ||
throw new Error("It is not allowed to reassign observable functions"); | ||
} | ||
} else { | ||
Object.defineProperty(target, key, { | ||
configurable: false, enumberable:true, | ||
get: function() { | ||
mobservableStatic.props(this, key, undefined); | ||
return this[key]; | ||
}, | ||
set: function(value) { | ||
mobservableStatic.props(this, key, value); | ||
} | ||
}); | ||
} | ||
} | ||
/** | ||
Evaluates func and return its results. Watch tracks all observables that are used by 'func' | ||
and invokes 'onValidate' whenever func *should* update. | ||
Returns a tuplde [return value of func, disposer]. The disposer can be used to abort the watch early. | ||
*/ | ||
mobservableStatic.watch = function watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda] { | ||
var dnode = new DNode(true); | ||
var retVal:T; | ||
dnode.nextState = function() { | ||
retVal = func(); | ||
dnode.nextState = function() { | ||
dnode.dispose(); | ||
onInvalidate(); | ||
return false; | ||
* Inverse function of `props` and `array`, given an (observable) array, returns a plain, | ||
* non observable version. (non recursive), or given an object with observable properties, returns a clone | ||
* object with plain properties. | ||
* | ||
* Any other value will be returned as is. | ||
*/ | ||
mobservableStatic.toPlainValue = function toPlainValue(value:any):any { | ||
if (value) { | ||
if (value instanceof Array) | ||
return value.slice(); | ||
else if (value instanceof ObservableValue) | ||
return value.get(); | ||
else if (typeof value === "function" && value.impl) { | ||
if (value.impl instanceof ObservableValue) | ||
return value() | ||
else if (value.impl instanceof ObservableArray) | ||
return value().slice(); | ||
} | ||
else if (typeof value === "object") { | ||
var res = {}; | ||
for (var key in value) | ||
res[key] = toPlainValue(value[key]); | ||
return res; | ||
} | ||
return false; | ||
} | ||
dnode.computeNextState(); | ||
return [retVal, () => dnode.dispose()]; | ||
return value; | ||
} | ||
@@ -119,6 +221,6 @@ | ||
(Since properties do not expose an .observe method themselves). | ||
*/ | ||
*/ | ||
mobservableStatic.observeProperty = function observeProperty(object:Object, key:string, listener:(...args:any[])=>void, invokeImmediately = false):Lambda { | ||
if (!object || !key || object[key] === undefined) | ||
throw new Error(`Object '${object}' has no key '${key}'.`); | ||
throw new Error(`Object '${object}' has no property '${key}'.`); | ||
if (!listener || typeof listener !== "function") | ||
@@ -133,4 +235,4 @@ throw new Error("Third argument to mobservable.observeProperty should be a function"); | ||
// IObservable? -> attach observer | ||
else if (currentValue.prop && currentValue.prop instanceof ObservableValue) | ||
return currentValue.prop.observe(listener, invokeImmediately); | ||
else if (currentValue.impl && (currentValue.impl instanceof ObservableValue || currentValue instanceof ObservableArray)) | ||
return currentValue.impl.observe(listener, invokeImmediately); | ||
@@ -150,13 +252,12 @@ // wrap with observable function | ||
mobservableStatic.array = function array<T>(values?:T[]): ObservableArray<T> { | ||
return new ObservableArray(values); | ||
/** | ||
Evaluates func and return its results. Watch tracks all observables that are used by 'func' | ||
and invokes 'onValidate' whenever func *should* update. | ||
Returns a tuplde [return value of func, disposer]. The disposer can be used to abort the watch early. | ||
*/ | ||
mobservableStatic.watch = function watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda] { | ||
var watch = new WatchedExpression(func, onInvalidate); | ||
return [watch.value, () => watch.dispose()]; | ||
} | ||
mobservableStatic.toJSON = function toJSON<T>(value:T):T { | ||
if (value instanceof ObservableArray) | ||
return (<any>value).values(); | ||
// later on, more cases like objects and such | ||
return value; | ||
} | ||
mobservableStatic.batch = function batch<T>(action:()=>T):T { | ||
@@ -166,85 +267,12 @@ return Scheduler.batch(action); | ||
mobservableStatic.onReady = function onReady(listener:Lambda):Lambda { | ||
return Scheduler.onReady(listener); | ||
} | ||
mobservableStatic.debugLevel = 0; | ||
mobservableStatic.onceReady = function onceReady(listener:Lambda) { | ||
Scheduler.onceReady(listener); | ||
} | ||
mobservableStatic.observable = function observable(target:Object, key:string, descriptor?) { | ||
var baseValue = descriptor ? descriptor.value : null; | ||
// observable annotations are invoked on the prototype, not on actual instances, | ||
// so upon invocation, determine the 'this' instance, and define a property on the | ||
// instance as well (that hides the propotype property) | ||
if (typeof baseValue === "function") { | ||
delete descriptor.value; | ||
delete descriptor.writable; | ||
descriptor.get = function() { | ||
mobservableStatic.defineObservableProperty(this, key, baseValue); | ||
return this[key]; | ||
} | ||
descriptor.set = function () { | ||
console.trace(); | ||
throw new Error("It is not allowed to reassign observable functions"); | ||
} | ||
} else { | ||
Object.defineProperty(target, key, { | ||
configurable: true, enumberable:true, | ||
get: function() { | ||
mobservableStatic.defineObservableProperty(this, key, undefined); | ||
return this[key]; | ||
}, | ||
set: function(value) { | ||
if (Array.isArray(value)) { | ||
var ar = new ObservableArray(value); | ||
Object.defineProperty(this, key, { | ||
value: ar, | ||
writeable: false, | ||
configurable: false, | ||
enumberable: true | ||
}); | ||
} | ||
else | ||
mobservableStatic.defineObservableProperty(this, key, value); | ||
} | ||
}); | ||
} | ||
} | ||
mobservableStatic.defineObservableProperty = function defineObservableProperty<T>(object:Object, name:string, initialValue?:T) { | ||
var _property = mobservableStatic.value(initialValue, object); | ||
definePropertyForObservable(object, name, _property); | ||
} | ||
mobservableStatic.initializeObservableProperties = function initializeObservableProperties(object:Object) { | ||
for(var key in object) if (object.hasOwnProperty(key)) { | ||
if (object[key] && object[key].prop && object[key].prop instanceof ObservableValue) | ||
definePropertyForObservable(object, key, <IObservableValue<any,any>> object[key]) | ||
} | ||
} | ||
function definePropertyForObservable(object:Object, name:string, observable:IObservableValue<any,any>) { | ||
Object.defineProperty(object, name, { | ||
get: function() { | ||
return observable(); | ||
}, | ||
set: function(value) { | ||
observable(value); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
} | ||
class ObservableValue<T,S> { | ||
class ObservableValue<T> { | ||
protected changeEvent = new SimpleEventEmitter(); | ||
protected dependencyState:DNode = new DNode(false); | ||
protected dependencyState:DNode = new DNode(this); | ||
constructor(protected _value?:T, protected scope?:S){ | ||
constructor(protected _value?:T){ | ||
} | ||
set(value:T):S { | ||
set(value:T) { | ||
if (value !== this._value) { | ||
@@ -257,3 +285,2 @@ var oldValue = this._value; | ||
} | ||
return this.scope; | ||
} | ||
@@ -276,2 +303,16 @@ | ||
} | ||
createGetterSetter():IObservableValue<T> { | ||
var self = this; | ||
var f:any = function(value?) { | ||
if (arguments.length > 0) | ||
self.set(value); | ||
else | ||
return self.get(); | ||
}; | ||
f.observe = (listener, fire) => this.observe(listener, fire); | ||
f.impl = this; | ||
f.toString = () => this.toString(); | ||
return f; | ||
} | ||
@@ -283,12 +324,10 @@ toString() { | ||
class ComputedObservable<U,S> extends ObservableValue<U,S> { | ||
class ComputedObservable<U> extends ObservableValue<U> { | ||
private isComputing = false; | ||
private hasError = false; | ||
constructor(protected func:()=>U, scope:S) { | ||
super(undefined, scope); | ||
if (!func) | ||
constructor(protected func:()=>U, private scope?:Object) { | ||
super(undefined); | ||
if (typeof func !== "function") | ||
throw new Error("ComputedObservable requires a function"); | ||
this.dependencyState.isComputed = true; | ||
this.dependencyState.nextState = this.compute.bind(this); | ||
} | ||
@@ -299,3 +338,3 @@ | ||
throw new Error("Cycle detected"); | ||
var state = this.dependencyState; | ||
var state = this.dependencyState; | ||
if (state.isSleeping) { | ||
@@ -327,3 +366,3 @@ if (DNode.trackingStack.length > 0) { | ||
set(_:U):S { | ||
set(_:U) { | ||
throw new Error(this.toString() + ": A computed observable does not accept new values!"); | ||
@@ -366,2 +405,31 @@ } | ||
/** | ||
* given an expression, evaluate it once and track its dependencies. | ||
* Whenever the expression *should* re-evaluate, the onInvalidate event should fire | ||
*/ | ||
class WatchedExpression<T> { | ||
private dependencyState = new DNode(this); | ||
private didEvaluate = false; | ||
public value:T; | ||
constructor(private expr:()=>T, private onInvalidate:()=>void){ | ||
this.dependencyState.computeNextState(); | ||
} | ||
compute() { | ||
if (!this.didEvaluate) { | ||
this.didEvaluate = true; | ||
this.value = this.expr(); | ||
} else { | ||
this.dispose(); | ||
this.onInvalidate(); | ||
} | ||
return false; | ||
} | ||
dispose() { | ||
this.dependencyState.dispose(); | ||
} | ||
} | ||
enum DNodeState { | ||
@@ -392,5 +460,6 @@ STALE, // One or more depencies have changed but their values are not yet known, current value is stale | ||
private externalRefenceCount = 0; // nr of 'things' that depend on us, excluding other DNode's. If > 0, this node will not go to sleep | ||
public isComputed:boolean;; // isComputed indicates that this node can depend on others, and should update when dependencies change | ||
constructor(public isComputed:boolean) { | ||
// isComputed indicates that this node can depend on others. | ||
constructor(private owner:{compute?:()=>boolean}) { | ||
this.isComputed = owner.compute !== undefined; | ||
} | ||
@@ -431,4 +500,2 @@ | ||
this.notifyObservers(stateDidActuallyChange); | ||
if (this.observers.length === 0) // otherwise, let one of the observers do that :) | ||
Scheduler.scheduleReady(); | ||
} | ||
@@ -443,3 +510,3 @@ | ||
tryToSleep() { | ||
if (this.isComputed && this.observers.length === 0 && this.externalRefenceCount === 0 && !this.isSleeping) { | ||
if (!this.isSleeping && this.isComputed && this.observers.length === 0 && this.externalRefenceCount === 0) { | ||
for (var i = 0, l = this.observing.length; i < l; i++) | ||
@@ -460,2 +527,3 @@ this.observing[i].removeObserver(this); | ||
// the state of something we are observing has changed.. | ||
notifyStateChange(observable:DNode, stateDidActuallyChange:boolean) { | ||
@@ -465,6 +533,6 @@ if (observable.state === DNodeState.STALE) { | ||
this.markStale(); | ||
} else { // ready | ||
} else { // not stale, thus ready since pending states are not propagated | ||
if (stateDidActuallyChange) | ||
this.dependencyChangeCount += 1; | ||
if (--this.dependencyStaleCount === 0) { | ||
if (--this.dependencyStaleCount === 0) { // all dependencies are ready | ||
this.state = DNodeState.PENDING; | ||
@@ -486,3 +554,3 @@ Scheduler.schedule(() => { | ||
this.trackDependencies(); | ||
var stateDidChange = this.nextState(); | ||
var stateDidChange = this.owner.compute(); | ||
this.bindDependencies(); | ||
@@ -492,6 +560,2 @@ this.markReady(stateDidChange); | ||
nextState():boolean { | ||
return false; // false == unchanged | ||
} | ||
private trackDependencies() { | ||
@@ -561,3 +625,8 @@ this.prevObserving = this.observing; | ||
class ObservableArray<T> implements Array<T> { | ||
// Workaround to make sure ObservableArray extends Array | ||
class StubArray { | ||
} | ||
StubArray.prototype = []; | ||
class ObservableArray<T> extends StubArray implements IObservableArray<T> { | ||
[n: number]: T; | ||
@@ -570,7 +639,8 @@ | ||
constructor(initialValues?:T[]) { | ||
super(); | ||
// make for .. in / Object.keys behave like an array, so hide the other properties | ||
Object.defineProperties(this, { | ||
"dependencyState" : { enumerable: false, value: new DNode(false) }, | ||
"dependencyState" : { enumerable: false, value: new DNode(this) }, | ||
"_values" : { enumerable: false, value: initialValues ? initialValues.slice() : [] }, | ||
"changeEvent" : { enumerable: false, value: new SimpleEventEmitter() }, | ||
"changeEvent" : { enumerable: false, value: new SimpleEventEmitter() } | ||
}); | ||
@@ -645,3 +715,3 @@ if (initialValues && initialValues.length) | ||
// conform: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe | ||
this.changeEvent.emit({ object: this, type: 'update', index: index, oldValue: oldValue}); | ||
this.changeEvent.emit(<IArrayChange<T>>{ object: this, type: 'update', index: index, oldValue: oldValue}); | ||
} | ||
@@ -654,3 +724,3 @@ | ||
// conform: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe | ||
this.changeEvent.emit({ object: this, type: 'splice', index: index, addedCount: added.length, removed: deleted}); | ||
this.changeEvent.emit(<IArraySplice<T>>{ object: this, type: 'splice', index: index, addedCount: added.length, removed: deleted}); | ||
} | ||
@@ -663,5 +733,5 @@ | ||
observe(listener:(data)=>void, fireImmediately=false):Lambda { | ||
observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately=false):Lambda { | ||
if (fireImmediately) | ||
listener({ object: this, type: 'splice', index: 0, addedCount: this._values.length, removed: []}); | ||
listener(<IArraySplice<T>>{ object: this, type: 'splice', index: 0, addedCount: this._values.length, removed: []}); | ||
return this.changeEvent.on(listener); | ||
@@ -693,6 +763,20 @@ } | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find | ||
find(predicate:(item:T,index:number,array:ObservableArray<T>)=>boolean, thisArg?, fromIndex=0):T { | ||
this.dependencyState.notifyObserved(); | ||
var items = this._values, l = items.length; | ||
for(var i = fromIndex; i < l; i++) | ||
if(predicate.call(thisArg, items[i], i, this)) | ||
return items[i]; | ||
return null; | ||
} | ||
/* | ||
functions that do alter the internal structure of the array, from lib.es6.d.ts | ||
functions that do alter the internal structure of the array, (based on lib.es6.d.ts) | ||
since these functions alter the inner structure of the array, the have side effects. | ||
Because the have side effects, they should not be used in computed function, | ||
and for that reason the do not call dependencyState.notifyObserved | ||
*/ | ||
splice(index:number, deleteCount?:number, ...newItems:T[]):T[] { | ||
this.sideEffectWarning("splice"); | ||
switch(arguments.length) { | ||
@@ -710,2 +794,3 @@ case 0: | ||
push(...items: T[]): number { | ||
this.sideEffectWarning("push"); | ||
this.spliceWithArray(this._values.length, 0, items); | ||
@@ -716,2 +801,3 @@ return this._values.length; | ||
pop(): T { | ||
this.sideEffectWarning("pop"); | ||
return this.splice(Math.max(this._values.length - 1, 0), 1)[0]; | ||
@@ -721,2 +807,3 @@ } | ||
shift(): T { | ||
this.sideEffectWarning("shift"); | ||
return this.splice(0, 1)[0] | ||
@@ -726,2 +813,3 @@ } | ||
unshift(...items: T[]): number { | ||
this.sideEffectWarning("unshift"); | ||
this.spliceWithArray(0, 0, items); | ||
@@ -732,2 +820,3 @@ return this._values.length; | ||
reverse():T[] { | ||
this.sideEffectWarning("reverse"); | ||
return this.replace(this._values.reverse()); | ||
@@ -737,4 +826,16 @@ } | ||
sort(compareFn?: (a: T, b: T) => number): T[] { | ||
this.sideEffectWarning("sort"); | ||
return this.replace(this._values.sort.apply(this._values, arguments)); | ||
} | ||
remove(value:T):boolean { | ||
this.sideEffectWarning("remove"); | ||
var idx = this._values.indexOf(value); | ||
if (idx > -1) { | ||
this.splice(idx, 1); | ||
return true; | ||
} | ||
return false; | ||
} | ||
/* | ||
@@ -768,2 +869,7 @@ functions that do not alter the array, from lib.es6.d.ts | ||
private sideEffectWarning(funcName:string) { | ||
if (DNode.trackingStack.length > 0) | ||
warn(`[Mobservable.Array] The method array.${funcName} should not be used inside observable functions since it has side-effects`); | ||
} | ||
static OBSERVABLE_ARRAY_BUFFER_SIZE = 0; | ||
@@ -775,3 +881,3 @@ static ENUMERABLE_PROPS = []; | ||
enumerable: false, | ||
configurable: true, | ||
configurable: false, | ||
set: function(value) { | ||
@@ -800,2 +906,3 @@ if (index < this._values.length) { | ||
prop.enumerable = true; | ||
prop.configurable = true; | ||
ObservableArray.ENUMERABLE_PROPS[index] = prop; | ||
@@ -812,3 +919,3 @@ } | ||
class SimpleEventEmitter { | ||
class SimpleEventEmitter implements ISimpleEventEmitter { | ||
listeners:{(data?):void}[] = []; | ||
@@ -856,4 +963,2 @@ | ||
class Scheduler { | ||
private static pendingReady = false; | ||
private static readyEvent = new SimpleEventEmitter(); | ||
private static inBatch = 0; | ||
@@ -871,12 +976,12 @@ private static tasks:{():void}[] = []; | ||
var i = 0; | ||
try { | ||
for(; i < Scheduler.tasks.length; i++) | ||
Scheduler.tasks[i](); | ||
Scheduler.tasks = []; | ||
} catch (e) { | ||
console.error("Failed to run scheduled action, the action has been dropped from the queue: " + e, e); | ||
// drop already executed tasks, including the failing one, and retry in the future | ||
Scheduler.tasks.splice(0, i + 1); | ||
setTimeout(Scheduler.runPostBatchActions, 1); | ||
throw e; // rethrow | ||
while(Scheduler.tasks.length) { | ||
try { // try outside loop; much cheaper | ||
for(; i < Scheduler.tasks.length; i++) | ||
Scheduler.tasks[i](); | ||
Scheduler.tasks = []; | ||
} catch (e) { | ||
console.error("Failed to run scheduled action, the action has been dropped from the queue: " + e, e); | ||
// drop already executed tasks, including the failing one, and continue with other actions, to keep state as stable as possible | ||
Scheduler.tasks.splice(0, i + 1); | ||
} | ||
} | ||
@@ -892,25 +997,9 @@ } | ||
if (--Scheduler.inBatch === 0) { | ||
// make sure follow up actions are processed in batch after the current queue | ||
Scheduler.inBatch += 1; | ||
Scheduler.runPostBatchActions(); | ||
Scheduler.scheduleReady(); | ||
Scheduler.inBatch -= 1; | ||
} | ||
} | ||
} | ||
static scheduleReady() { | ||
if (!Scheduler.pendingReady) { | ||
Scheduler.pendingReady = true; | ||
setTimeout(() => { | ||
Scheduler.pendingReady = false; | ||
Scheduler.readyEvent.emit(); | ||
}, 1); | ||
} | ||
} | ||
static onReady(listener:Lambda) { | ||
return Scheduler.readyEvent.on(listener); | ||
} | ||
static onceReady(listener:Lambda) { | ||
return Scheduler.readyEvent.once(listener); | ||
} | ||
} | ||
@@ -1011,2 +1100,24 @@ | ||
export = mobservableStatic; | ||
} // end of module | ||
/* typescript does not support UMD modules yet, lets do it ourselves... */ | ||
declare var define; | ||
declare var exports; | ||
declare var module; | ||
(function (root, factory) { | ||
if (typeof define === 'function' && define.amd) { | ||
// AMD. | ||
define('mobservable', [], function () { | ||
return (factory()); | ||
}); | ||
} else if (typeof exports === 'object') { | ||
// CommonJS like | ||
module.exports = factory(); | ||
} else { | ||
// register global | ||
root['mobservable'] = factory(); | ||
} | ||
}(this, function () { | ||
return mobservable.mobservableStatic; | ||
})); |
{ | ||
"name": "mobservable", | ||
"version": "0.3.3", | ||
"description": "Changes are coming! Small library for creating observable properties en functions", | ||
"version": "0.4.0", | ||
"description": "Changes are coming! Small library for creating observable properties, arrays and functions", | ||
"main": "dist/mobservable.js", | ||
"scripts": { | ||
"test": "grunt cover", | ||
"prepublish": "grunt build" | ||
"prepublish": "grunt builddist" | ||
}, | ||
@@ -24,7 +24,22 @@ "repository": { | ||
"grunt-coveralls": "^1.0.0", | ||
"grunt-ts": "^4.0.1", | ||
"grunt-exec": "^0.4.6", | ||
"grunt-contrib-uglify": "^0.9.1", | ||
"mkdirp": "^0.5.1", | ||
"nodeunit-browser-tap": "^0.1.0", | ||
"nscript": "^0.1.5", | ||
"typescript": "^1.5.0-alpha" | ||
"typescript": "^1.5.0-beta" | ||
}, | ||
"testling": { | ||
"files": "test/browser/test.js", | ||
"browsers": [ | ||
"ie/6..latest", | ||
"chrome/22..latest", | ||
"firefox/16..latest", | ||
"safari/latest", | ||
"opera/11.0..latest", | ||
"iphone/6", | ||
"ipad/6", | ||
"android-browser/latest" | ||
] | ||
} | ||
} |
550
README.md
# MOBservable | ||
*Changes are coming!* | ||
MOBservable is light-weight stand-alone observable implementation, based on the ideas of observables in bigger frameworks like `knockout`, `ember`, but this time without 'strings attached'. MOBservables allows you to observe primitive values, references, functions and arrays. | ||
[![Build Status](https://travis-ci.org/mweststrate/MOBservable.svg?branch=master)](https://travis-ci.org/mweststrate/MOBservable) | ||
@@ -12,281 +8,477 @@ [![Coverage Status](https://coveralls.io/repos/mweststrate/MOBservable/badge.svg?branch=master)](https://coveralls.io/r/mweststrate/MOBservable) | ||
[Typescript typings](https://github.com/mweststrate/MOBservable/blob/master/mobservable.d.ts) | ||
Installation: `npm install mobservable --save` | ||
# Observable values | ||
MOBservable is light-weight stand-alone observable implementation, that helps you to create reactive data structures, based on the ideas of observables in bigger frameworks like `knockout`, `ember`, but this time without 'strings attached'. | ||
MOBservables allows you to observe primitive values, references, functions and arrays and makes sure that all changes in your data are propagated automatically, atomically and synchronously. | ||
The `mobservable.value(valueToObserve)` method (or just its shorthand: `mobservable(valueToObserve)`) takes a value or function and creates an observable value from it. A quick example: | ||
# Examples | ||
```typescript | ||
/// <reference path='./node_modules/mobservable/mobservable.d.ts'/> | ||
import mobservable = require('mobservable'); | ||
[Fiddle demo: MOBservable + JQuery](http://jsfiddle.net/mweststrate/vxn7qgdw) | ||
var vat = mobservable.value(0.20); | ||
## Example: Observable values and functions | ||
var order = {}; | ||
order.price = mobservable.value(10), | ||
order.priceWithVat = mobservable.value(() => order.price() * (1 + vat())); | ||
The core of `MOBservable` consists of observable values, functions that automatically recompute when an observed value changes, | ||
and the possibility to listen to changing values and updated computations. | ||
order.priceWithVat.observe((price) => console.log("New price: " + price)); | ||
```javascript | ||
var mobservable = require('mobservable'); | ||
order.price(20); | ||
// Prints: New price: 24 | ||
vat(0.10); | ||
// Prints: New price: 22 | ||
var nrOfCatz = mobservable(3); | ||
var nrOfDogs = mobservable(8); | ||
// Create a function that automatically observes values: | ||
var nrOfAnimals = mobservable(function() { | ||
// calling an mobservable without arguments acts as getter | ||
return nrOfCatz() * nrOfDogs(); | ||
}); | ||
// Print a message whenever the observable changes: | ||
nrOfAnimals.observe(function(amount) { | ||
console.log("Total: " + amount); | ||
}, true); | ||
// -> Prints: "Total: 11" | ||
// calling an mobservable with a value acts as setter, | ||
// ...and automatically updates all computations in which it was used | ||
nrOfCatz(34); | ||
// -> Prints: "Total: 42" | ||
``` | ||
## mobservable.value(value, scope?):IObservableValue | ||
## Example: Observable objects & properties | ||
Constructs a new observable value. The value can be everything that is not a function, or a function that takes no arguments and returns a value. In the body of the function, references to other properties will be tracked, and on change, the function will be re-evaluated. The returned value is an `IProperty` function/object. Passing an array or object into the `value` method will only observe the reference, not the contents of the objects itself. To observe the contents of an array, use `mobservable.array`, to observe the contents of an object, just make sure its (relevant) properties are observable values themselves. | ||
By using `.props`, it is possible to create observable values and functions that can be assigned or read as normal properties. | ||
The method optionally accepts a scope parameter, which will be returned by the setter for chaining, and which will be used as scope for calculated properties, for example: | ||
```javascript | ||
var value = mobservable.value; | ||
var mobservable = require('mobservable'); | ||
function OrderLine(price, amount) { | ||
this.price = value(price); | ||
this.amount = value(amount); | ||
this.total = value(function() { | ||
return this.price() * this.amount(); | ||
}, this) | ||
var Person = function(firstName, lastName) { | ||
// define the observable properties firstName, lastName and fullName on 'this'. | ||
mobservable.props(this, { | ||
firstName: firstName, | ||
lastName: lastName, | ||
fullName: function() { | ||
return this.firsName + " " + this.lastName; | ||
} | ||
}); | ||
} | ||
var jane = new Person("Jane","Dôh"); | ||
// (computed) properties can be accessed like any other property: | ||
console.log(jan.fullName); | ||
// prints: "Jan Dôh" | ||
// properties can be observed as well: | ||
mobsevable.observeProperty(jane, "fullName", console.log); | ||
// values can be assigned directly to observable properties | ||
jane.lastName = "Do"; | ||
// prints: "Jane Do" | ||
``` | ||
**Note: `mobservable.value` versus `mobservable.array`** | ||
Do *not* confuse `mobservable.value([])` (or `mobservable([])`) with `mobservable.array([])`, the first creates an observable reference to an array, but does not observe its contents. The later observes the contents from the array you pass into it. | ||
## Example: Observable arrays | ||
## mobservable.array(initialValues?):ObservableArray | ||
`mobservable` provides an observable array implementation, which is fully ES5 compliant, | ||
but which will notify dependent computations upon each change. | ||
**Note: ES5 environments only** | ||
```javascript | ||
import mobservable = require('mobservable'); | ||
Constructs an array like, observable structure. An observable array is a thin abstraction over native arrays that adds observable properties. The only noticable difference between built-in arrays is that these arrays cannot be sparse, that is, values assigned to an index larger than `length` are not oberved (nor any other property that is assigned to a non-numeric index). In practice, this should harldy be an issue. Example: | ||
// create an array, that works by all means as a normal array, except that it is observable! | ||
var someNumbers = mobservable.value([1,2,3]); | ||
```javascript | ||
var numbers = mobservable.array([1,2,3]); | ||
// a naive function that sums all the values | ||
var sum = mobservable.value(function() { | ||
return numbers.reduce(function(a, b) { return a + b }, 0); | ||
for(var s = 0, i = 0; i < someNumbers.length; i++) | ||
s += someNumbers[i]; | ||
return s; | ||
}); | ||
sum.observe(function(s) { console.log(s); }); | ||
sum.observe(console.log); | ||
numbers[3] = 4; | ||
// prints 10 | ||
numbers.push(5,6); | ||
// prints 21 | ||
numbers.unshift(10) | ||
// prints 31 | ||
someNumbers.push(4); | ||
// Prints: 10 | ||
someNumbers[2] = 0; | ||
// Prints: 7 | ||
someNumbers[someNumbers.length] = 5; | ||
// Prints: 12 | ||
``` | ||
**Note: do not reassign a array variables!** | ||
## Example: TypeScript classes and annotations | ||
In general you should never (need to) reassign variables that hold an observable array, instead, use the `replace` method on the array. If you reassign a variable that holds an observable array, the reassignment won't be visible to any of it observers; they will still be observing the original array: | ||
For typescript users, `mobservable` ships with module typings and an `@observable` annotation with which class members can be marked as observable. | ||
```javascript | ||
var numbers = mobservable.array([1,2]); | ||
// .. stuff that depends on numbers | ||
// bad: | ||
var numbers = mobservable.array([1,2,3]); | ||
// good: | ||
numbers.replace([1,2,3]); | ||
``` | ||
```typescript | ||
/// <reference path="./node_modules/mobservable/mobservable.d.ts"/> | ||
import mobservable = require('mobservable'); | ||
var observable = mobservable.observable; | ||
## mobservable.Observable annotation | ||
class Order { | ||
@observable orderLines: OrderLine[] = []; | ||
@observable total() { | ||
return this.orderLines.reduce((sum, orderLine) => sum + orderLine.total, 0) | ||
} | ||
} | ||
**Note: ES5, TypeScript 1.5+ environments only** | ||
class OrderLine { | ||
@observable price:number = 0; | ||
@observable amount:number = 1; | ||
Marks a property or method as observable. This annotations basically wraps `mobservable.defineObservableProperty`. If the annotations is used in combination with an array property, an observable array will be created. | ||
constructor(price) { | ||
this.price = price; | ||
} | ||
```typescript | ||
var observable = require('mobservable').observable; | ||
class Order { | ||
@observable price:number = 3; | ||
@observable amount:number = 2; | ||
@observable orders = []; | ||
@observable total() { | ||
return this.amount * this.price * (1 + orders.length); | ||
return "Total: " + this.price * this.amount; | ||
} | ||
} | ||
} | ||
var order1 = new Order(); | ||
order1.total.observe(console.log); | ||
order1.orderLines.push(new OrderLine(7)); | ||
// Prints: Total: 7 | ||
order1.orderLines.push(new OrderLine(12)); | ||
// Prints: Total: 12 | ||
order1.orderLines[0].amount = 3; | ||
// Prints: Total: 33 | ||
``` | ||
## mobservable.defineObservableProperty(object, name, value) | ||
# Processing observables | ||
**Note: ES5 environments only** | ||
Observable values, arrays and functions created by `mobservable` possess the following characteristics: | ||
Defines a property using ES5 getters and setters. This is useful in constructor functions, and allows for direct assignment / reading from observables: | ||
* _synchronous_. All updates are processed synchronously, that is, the pseudo expressions `a = 3; b -> a * 2; a = 4; print(b); ` will always print `4`; `b` will never yield a stale value (unless `batch` is used). | ||
* _atomic_. All computed values will postpone updates until all inputs are settled, to make sure no intermediate values are visible. That is, the expression `a = 3; b -> a * 2; c -> a * b; a = 4; print(c)` will always print `36` and no intermediate values like `24`. | ||
* _real time dependency detection_. Computed values only depend on values actually used in the last computation, for example in this `a -> b > 5 ? c : b` the variable `c` will only cause a re-evaluation of a if `b` > 5. | ||
* _lazy_. Computed values will only be evaluated if they are actually being observed. So make sure computed functions are pure and side effect free; the library might not evaluate the expression as often as you thought it would. | ||
* _cycle detection_. Cycles in computes, like in `a -> 2 * b; b -> 2 * a;` will be deteced automatically. | ||
* _error handling_. Exceptions that are raised during computations are propagated to consumers. | ||
```javascript | ||
var vat = mobservable.value(0.2); | ||
# API | ||
var Order = function() { | ||
mobservable.defineObservableProperty(this, 'price', 20); | ||
mobservable.defineObservableProperty(this, 'amount', 2); | ||
mobservable.defineObservableProperty(this, 'total', function() { | ||
return (1+vat()) * this.price * this.amount; // price and amount are now properties! | ||
}); | ||
}; | ||
[Typescript typings](https://github.com/mweststrate/MOBservable/blob/master/mobservable.d.ts) | ||
var order = new Order(); | ||
order.price = 10; | ||
order.amount = 3; | ||
// order.total now equals 36 | ||
## Creating observables | ||
### mobservable | ||
Shorthand for `mobservable.value` | ||
### mobservable.value | ||
`mobservable.value<T>(value? : T[], scope? : Object) : IObservableArray<T>` | ||
`mobservable.value<T>(value? : T|()=>T, scope? : Object) : IObservableValue<T>` | ||
Function that creates an observable given a `value`. | ||
Depending on the type of the function, this function invokes `mobservable.array`, `mobservable.computed` or `mobservable.primitive`. | ||
See the examples above for usage patterns. The `scope` is only meaningful if a function is passed into this method. | ||
### mobservable.primitive | ||
`mobservable.primitive<T>(value? : T) : IObservableValue<T>` | ||
Creates a new observable, initialzed with the given `value` that can change over time. | ||
The returned observable is a function, that without arguments acts as getter, and with arguments as setter. | ||
Furthermore its value can be observed using the `.observe` method, see `IObservableValue.observe`. | ||
Example: | ||
``` | ||
var vat = mobservable.primitive(3); | ||
console.log(vat()); // prints '3' | ||
vat.observe(console.log); // register an observer | ||
vat(4); // updates value, also notifies all observers, thus prints '4' | ||
``` | ||
In typescript < 1.5, it might be more convenient for the typesystem to directly define getters and setters instead of using `mobservable.defineProperty` (or, use `mobservable.initializeObservableProperties`): | ||
```typescript | ||
class Order { | ||
_price = new mobservable.value(20, this); | ||
get price() { | ||
return this._price(); | ||
} | ||
set price(value) { | ||
this._price(value); | ||
} | ||
} | ||
### mobservable.reference | ||
`mobservable.reference<T>(value? : T) : IObservableValue<T>` | ||
Synonym for `mobservable.primitive`, since the equality of primitives is determined in the same way as references, namely by strict equality. | ||
(from version 0.6, see `mobservable.struct` if values need to be compared structuraly by using deep equality). | ||
### mobservable.computed | ||
`mobservable.computed<T>(expr : () => T, scope?) : IObservableValue<T>` | ||
`computed` turns a function into an observable value. | ||
The provided `expr` should not have any arguments, but instead really on other observables that are in scope to determine its value. | ||
The latest value returned by `expr` determines the value of the observable. When one of the observables used in `expr` changes, `computed` will make sure that the function gets re-evaluated, and all updates are propogated to the children. | ||
```javascript | ||
var amount = mobservable(3); | ||
var price = mobservable(2); | ||
var total = mobservable.computed(function() { | ||
return amount() * price(); | ||
}); | ||
console.log(total()); // gets the current value, prints '6' | ||
total.observe(console.log); // attach listener | ||
amount(4); // update amount, total gets re-evaluated automatically and will print '8' | ||
amount(4); // update amount, but total will not be re-evaluated since the value didn't change | ||
``` | ||
## mobservable.initializeObservableProperties(object) | ||
The optional `scope` parameter defines `this` context during the evaluation of `expr`. | ||
`computed` will try to reduce the amount of re-evaluates of `expr` as much as possible. For that reason the function *should* be pure, that is: | ||
* The result of `expr` should only be defined in terms of other observables, and not depend on any other state. | ||
* Your code shouldn't rely on any side-effects, triggered by `expr`; `expr` should be side-effect free. | ||
* The result of `expr` should always be the same if none of the observed observables did change. | ||
It is not allowed for `expr` to have an (implicit) dependency on its own value. | ||
It is allowed to throw exceptions in an observed function. The thrown exceptions might only be detected late. | ||
The exception will be rethrown if somebody inspects the current value, and will be passed as first callback argument | ||
to all the listeners. | ||
### mobservable.array | ||
`mobservable.array<T>(values? : T[]) : IObservableArray<T>` | ||
**Note: ES5 environments only** | ||
Converts all observables of the given object into property accessors. For example: | ||
Constructs an array like, observable structure. An observable array is a thin abstraction over native arrays that adds observable properties. | ||
The most notable difference between built-in arrays is that these arrays cannot be sparse, that is, values assigned to an index larger than `length` are considered out-of-bounds and not oberved (nor any other property that is assigned to a non-numeric index). | ||
Furthermore, `Array.isArray(observableArray)` and `typeof observableArray === "array"` will yield `false` for observable arrays, but `observableArray instanceof Array` will return `true`. | ||
```javascript | ||
var Order = function() { | ||
this.price = value(20); | ||
this.amount = value(2); | ||
this.nonsense = 3; | ||
this.total = value(function() { | ||
return (1+vat()) * this.price * this.amount; // price and amount are now properties! | ||
}, this); | ||
mobservable.initializeObservableProperties(this); | ||
}; | ||
var numbers = mobservable.array([1,2,3]); | ||
var sum = mobservable.value(function() { | ||
return numbers.reduce(function(a, b) { return a + b }, 0); | ||
}); | ||
sum.observe(function(s) { console.log(s); }); | ||
var order = new Order(); | ||
console.log(order.total); // prints 36 | ||
numbers[3] = 4; | ||
// prints 10 | ||
numbers.push(5,6); | ||
// prints 21 | ||
numbers.unshift(10); | ||
// prints 31 | ||
``` | ||
Or in typescript pre 1.5, where annotations are not yet supported: | ||
Observable arrays implement all the ES5 array methods. Besides those, the following methods are available as well: | ||
```typescript | ||
class Order { | ||
price:number = <number><any>new mobservable.value(20, this); | ||
constructor() { | ||
mobservable.initializeObservableProperties(this); | ||
} | ||
} | ||
``` | ||
* `observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda` Listen to changes in this array. The callback will receive arguments that express an array splice or array change, conform the [ES7 proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe) | ||
* `clear(): T[]` Remove all current entries from the array | ||
* `replace(newItems:T[])` Replaces all existing entries in the array with new ones. | ||
* `values(): T[]` Returns a shallow clone of the array, similar to `.slice` | ||
* `clone(): IObservableArray<T>` Create a new observable array containing the same values | ||
* `find(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T` Find implementation, basically the same as the ES7 Array.find proposal, but with added `fromIndex` parameter. | ||
* `remove(value:T):boolean` Remove a single item by value from the array. Returns true if the item was found and removed. | ||
## mobservable.observeProperty | ||
`function observeProperty(object:Object, key:string, listener:Function, fireImmediately=false):Lambda` | ||
### mobservable.props | ||
Observes the observable property `key` of `object`. This is useful if you want to observe properties created using the `observable` annotation or the `defineObservableProperty` method, since for those properties their own `observe` method is not publicly available. | ||
```typescript | ||
class Order { | ||
@observable total = () => this.price * this.amount; | ||
} | ||
var order = new Order(); | ||
props(target:Object, name:string, initialValue: any):Object; | ||
props(target:Object, props:Object):Object; | ||
props(target:Object):Object; | ||
``` | ||
**Note: ES5 environments only** | ||
mobservable.observeProperty(order, 'total', (newPrice) => console.log("New price: " + newPrice)); | ||
Creates observable properties on the given `target` object. This function uses `mobservable.value` internally to create observables. | ||
Creating properties has as advantage that they are more convenient to use. See also [props or variables](#value_versus_props). | ||
The original `target`, with the added properties, is returned by this function. Functions used to created computed observables will automatically | ||
be bound to the correct `this`. | ||
```javascript | ||
var order = {}; | ||
mobservable.props(order, { | ||
amount: 3, | ||
price: 5, | ||
total: function() { | ||
return this.amount * this.price; // note that no setters are needed | ||
} | ||
}); | ||
order.amount = 4; | ||
console.log(order.total); // Prints '20' | ||
``` | ||
## mobservable.watch(func, onInvalidate) | ||
Note that observables created by `mobservable.props` do not expose an `.observe` method, to observe properties, see [`mobservable.observeProperty`](#mobservable_observeproperty) | ||
`watch` invokes `func` and returns a tuple consisting of the return value of `func` and an unsubscriber. `watch` will track which observables `func` was observing, but it will *not* recalculate `func` if necessary, instead, it will fire the `onInvalidate` callback to notify that the output of `func` can no longer be trusted. | ||
Other forms in which this function can be called: | ||
```javascript | ||
mobservable.props(order, "price", 3); // equivalent to mobservable.props(order, { price: 3 }); | ||
var order = mobservable.props({ price: 3}); // uses the original object as target, that is, all values in it are replaced by their observable counterparts | ||
``` | ||
The `onInvalidate` function will be called only once, after that, the watch has finished. To abort a watch, use the returned unsubscriber. | ||
### mobservable.observable annotation | ||
`Watch` is useful in functions where you want to have a function that responds to change, but where the function is actually invoked as side effect or part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive, for example in React component render methods | ||
**Note: ES5, TypeScript 1.5+ environments only** | ||
## mobservable.batch(workerFunction) | ||
Typescript 1.5 introduces annotations. The `mobservable.observable` annotation can be used to mark class properties and functions as observable. | ||
This annotations basically wraps `mobservable.props`. Example: | ||
Batch postpones the updates of computed properties until the (synchronous) `workerFunction` has completed. This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values, for example while refreshing a value from the database. | ||
```typescript | ||
/// <reference path='./node_modules/mobservable/mobservable.d.ts'/> | ||
var observable = require('mobservable').observable; | ||
## mobservable.onReady(listener) / mobservable.onceReady(listener) | ||
class Order { | ||
@observable price:number = 3; | ||
@observable amount:number = 2; | ||
@observable orders = []; | ||
The listener is invoked each time the complete model has become stable. The listener is always invoked asynchronously, so that even without `batch` the listener is only invoked after a bunch of changes have been applied | ||
@observable total() { | ||
return this.amount * this.price * (1 + orders.length); | ||
} | ||
} | ||
``` | ||
## Observing changes | ||
`onReady` returns a function with wich the listener can be unsubscribed from future events | ||
### mobservable.observeProperty | ||
`mobservable.observeProperty(object : Object, key : string, listener : Function, invokeImmediately : boolean = false) : Function` | ||
## `IObservableValue` objects | ||
Observes the observable property `key` of `object`. This is useful if you want to observe properties created using the `observable` annotation or the `props` method, | ||
since for those properties their own `observe` method is not publicly available. | ||
### IObservableValue() | ||
```javascript | ||
function OrderLine(price) { | ||
mobservable.props(this, { | ||
price: price, | ||
amount: 2, | ||
total: function() { | ||
return this.price * this.amount; | ||
} | ||
}); | ||
} | ||
If an IObservableValue object is called without arguments, the current value of the observer is returned | ||
var orderLine = new OrderLine(5); | ||
mobservable.observeProperty(order, 'total', console.log, true); // Prints: '10' | ||
``` | ||
### IObservableValue(newValue) | ||
### mobservable.watch | ||
`mobservable.watch<T>(func: () => T, onInvalidate : Function) : [T, Function];` | ||
If an IObservable object is called with arguments, the current value is updated. All current observers will be updated as well. | ||
`watch` is quite similar to `mobservable.computed`, but instead of re-evaluating `func` when one of its dependencies has changed, the `onInvalidate` function is triggered. | ||
So `func` will be evaluated only once, and as soon as its value has become stale, the `onInvalidate` callback is triggered. | ||
`watch` returns a tuple consisting of the initial return value of `func` and an unsubscriber to be able to abort the watch. | ||
The `onInvalidate` function will be called only once, after that, the watch has finished. | ||
### IObservableValue.observe(listener,fireImmediately=false) | ||
`watch` is useful in functions where you want to have a function that responds to change, | ||
but where the function is actually invoked as side effect or as part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive, | ||
for example in the `render` method of a React component. | ||
Registers a new listener to change events. Listener should be a function, its first argument will be the new value, and second argument the old value. | ||
### mobservable.batch | ||
Returns a function that upon invocation unsubscribes the listener from the property. | ||
`mobservable.batch<T>(workerFunction : ()=>T):T` | ||
## `ObservableArray` | ||
Batch postpones the updates of computed properties until the (synchronous) `workerFunction` has completed. | ||
This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values, | ||
for example while refreshing a data from the database. | ||
An `ObservableArray` is an array-like structure with all the typical behavior of arrays, so you can freely assign new values to (non-sparse) indexes, alter the length, call array functions like `map`, `filter`, `shift` etc. etc. All the ES5 features are in there. Additionally available methods: | ||
```javascript | ||
var amount = mobservable(3); | ||
var price = mobservable(2.5); | ||
var total = mobservable(function() { | ||
return amount() * price(); | ||
}); | ||
total.observe(console.log); | ||
### ObservableArray.clear() | ||
// without batch: | ||
amount(2); // Prints 5 | ||
price(3); // Prints 6 | ||
Removes all elements from the array and returns the removed elements. Shorthand for `ObservableArray.splice(0)` | ||
// with batch: | ||
mobservable.batch(function() { | ||
amount(3); | ||
price(4); | ||
}); | ||
// Prints 12, after completing the batch | ||
``` | ||
### ObservableArray.replace(newItemsArray) | ||
## Utilities | ||
Replaces all the items in the array with `newItemsArray`, and returns the old items. | ||
### mobservable.toPlainValue | ||
`mobservable.toPlainValue<T>(any:T):T;` | ||
### ObservableArray.spliceWithArray(index, deleteCount, newItemsArray) | ||
Converts a (possibly) observable value into a non-observablue value. | ||
For non-primitive values, this function will always return a shallow copy. | ||
Similar to `Array.splice`, but instead of accepting a variable amount of arguments, the third argument should be an array containing the new arguments. | ||
### mobservable.debugLevel | ||
### ObservableArray.observe(callback) | ||
Numeric property, setting this to value to '1' or higher will cause additional debug information to be printed. | ||
Register a callback that will be triggered every time the array is altered. A method to unregister the callback is returned. | ||
### mobservable.SimpleEventEmitter | ||
Utility class for managing an event. Its methods are: | ||
The events that are being fired adhere to the ES7 specs for Array.observe. The event data will be either a `splice` or `update` event, examples: | ||
* `emit(...data : any[])`. Invokes all registered listeners with the given arguments | ||
* `on(listener:(...data : any[]) => void) : () => void`. Registers a new callback that will be invoked on each `emit`. Returns a method that can be used to unsubscribe the listener. | ||
* `once(listener:(...data : any[]) => void) : () => void`. Similar to `.on`, but automatically removes the listener after one invocation. | ||
```javascript | ||
{ object: <array>, type: "update", index: 2, oldValue: 4 }, | ||
{ object: <array>, type: "splice", index: 1, addedCount: 2, removed: [4,1] }, | ||
``` | ||
# Tips & tricks | ||
### ObservableArray.values() | ||
## Use local variables in computations | ||
Returns all the values of this ObservableArray as native, non-observable, javascript array. The returned array is a shallow copy. | ||
Each time an observable value is read, there is a small performance overhead to keep the dependency tree of computations up to date. | ||
Although this might not be noticable in practice, if you want to squeeze the last bit of performance out of the library; | ||
use local variables as much as possible to reduce the amount of observable reads. | ||
This also holds for array entries and object properties created using `mobservable.props`. | ||
## mobservable.SimpleEventEmitter | ||
```javascript | ||
var firstName = mobservable('John'); | ||
var lastName = mobservable('Do'); | ||
Class that implements a simple event system. | ||
// Ok: | ||
var fullName = mobservable(function() { | ||
if (firstName()) | ||
return lastName() + ", " + firstName(); // another read of firstName.. | ||
return lastName(); | ||
} | ||
### SimpleEventEmitter.emit | ||
// Faster: | ||
var fullName = mobservable(function() { | ||
var first = firstName(), last = lastName(); | ||
if (first) | ||
return last+ ", " + first; | ||
return last; | ||
} | ||
``` | ||
`emit(...data:any[]):void;` | ||
## Use native array methods | ||
Fires the event represented by this SimpleEventEmitter. All arguments passed to `emit` are passed to the listeners. | ||
For performance, use built-in array methods as much as possible; | ||
a classic array for loop is registered as multiple reads, while a function call is registered as a single read. | ||
Alternatively, slicing the array before using it will also result in a single read. | ||
### SimpleEventEmitter.on | ||
```javascript | ||
var numbers = mobservable([1,2,3]); | ||
`on(listener:(...data:any[])=>void):Lambda;` | ||
// Ok: | ||
var sum1 = mobservable(function() { | ||
var s = 0; | ||
for(var i = 0; i < numbers.length; i++) // observable read | ||
s += numbers[i]; // observable reads | ||
return s; | ||
}); | ||
Subscribes a new event listener to this event emitter. The returned function can be used to unsubscribe. | ||
// Faster: | ||
var sum2 = mobservable(function() { | ||
var s = 0, localNumbers = numbers.slice(); // observable read | ||
for(var i = 0; i < localNumbers.length; i++) | ||
s += localNumbers[i]; | ||
return s; | ||
}); | ||
### SimpleEventEmitter.once | ||
// Also fast: | ||
var sum2 = mobservable(function() { | ||
return numbers.reduce(function(a, b) { // single observable read | ||
return a + b; | ||
}, 0); | ||
}); | ||
``` | ||
## `.value` versus `.props` | ||
`once(listener:(...data:any[])=>void):Lambda;` | ||
Using `mobservable.value` or `mobservable.props` to create observables inside objects might be a matter of taste. | ||
Here is a small comparison list between the two approaches. | ||
Similar to `on`, but the listener is fired only one time and disposed after that. | ||
| .value | .props | | ||
| ---- | ---| | ||
| ES3 complient | requires ES 5 | | ||
| explicit getter/setter functions: `obj.amount(2)` | object properties with implicit getter/setter: `obj.amount = 2 ` | | ||
| easy to make mistakes; e.g. `obj.amount = 3` instead of `obj.amount(3)`, or `7 * obj.amount` instead of `7 * obj.amount()` wilt both not achieve the intended behavior | Use property reads / assignments | | ||
| easy to observe: `obj.amount.observe(listener)` | `mobservable.observeProperty(obj,'amount',listener)` | | ||
# Using mobservable with Typescript | ||
## `.reference` versus `.array` | ||
Use the following import statement to have strongly typed mobservables in typescript: | ||
Do *not* confuse `mobservable.primitive([])` (or `mobservable([])`) with `mobservable.array([])`, | ||
the first creates an observable reference to an array, but does not observe its contents. | ||
The later observes the contents from the array you pass into it. | ||
```typescript | ||
/// <reference path='./node_modules/mobservable/mobservable.d.ts'/> | ||
import mobservable = require('mobservable'); | ||
``` | ||
Note that the `mobservable(value)` shorthand is not available in typescript, due to limitations in the combination of require statements and .d.ts references. use `mobservable.value(value)` instead. |
95
todo.md
* ~~lazy tests~~ | ||
* ~~fix warning in test~~ | ||
* ~~fix lazy cycles~~ | ||
* ~~error tests~~ | ||
* ~~typescript tests~~ | ||
* ~~rename defineProperty to defineObservableProperty~~ | ||
* ~~introduce 1.5 decorator. w00t! https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#decorators~~ | ||
* ~~introduce initializeProperties~~ | ||
* ~~implement and test observe() methods~~ | ||
* ~~toJSON~~ | ||
* ~~layout elses, rename properties.js -> observables.js~~ | ||
* ~~tabs to spaces everywhere~~ | ||
* ~~remove memwatch, make tests smaller?~~ | ||
* ~~drop event emitter, to make lib smaller and stand alone? https://github.com/joyent/node/blob/master/lib/events.js, note: clone listeners before invoking, note: document~~ | ||
* ~~array.observe conform~~ | ||
* ~~badges for build, coverage, npm~~ | ||
* ~~process remaining optimizations / todo's, document code~~ | ||
* coverage tests | ||
* minified version | ||
* ~~use typescript 1.5 spread operator~~ | ||
* ~~use console.trace() in logging where applicable~~ | ||
* add 'name' as parameter to observable.value, automatically set it when defining properties, use it in warnings / toString | ||
* IReactiveValue interface | ||
* IObservable interface | ||
* browser support test https://ci.testling.com/ | ||
* ~~use destructurings (for example quickdiff)~~ | ||
* examples | ||
* introduce extend / properties / defineProperties | ||
* describe properties in readme: | ||
@@ -39,34 +11,63 @@ - synchronous updates | ||
* optimizations | ||
- ~~pass compute function into DNode in computed observable~~ | ||
- computable's without observers should stay 'awake' after a computation (e.g. being inspected by .get), | ||
but then go to sleep if one of its dependencies has changed, so that subsequent reads on a computable are cheap even if there are no observers | ||
- ~~node.addObserver check if new observer doesn't equal the prevous one~~ | ||
- check if somewhere an array is filled that could be preallocate | ||
- ~~array: recycle properties (or not)~~ | ||
- ~~look into defineProperties / making property creating faster (especially arrays)~~ | ||
- ~~count stale dependencies, instead of looping each time whether all dependencies are ready again.~~ | ||
- collapse stale / ready notifications whenever possible | ||
- ~~find unmodifyable empty lists / objects and put in var~~ | ||
- ~~heuristic to make computables non-lazy if used frequently (something like, in computable, if (this.lazyReads > this.computesWithoutObservers) then never-go-to-sleep)~~ | ||
- combine multiple adds to array during batch into single splice | ||
- verify that combine multiple assignments to property during batch are combined into singleupdate | ||
- verify that multiple computations schedulings during batch result in single computation | ||
* ~~make sure array properties are read only~~ | ||
- go to sleep if there are no observers and a 'stale' notification comes in | ||
- notifyStateChange: avoid or remove scheduler.schedule, or strip closure by passing the DNode in | ||
- do not wrap around .value in .props, to use save some closures | ||
- create dnode.observers / observing lazy, to save memory? | ||
0.4 | ||
* License | ||
* implement array.sort & reverse properly, they do change the array | ||
* drop mobservable.onReady / onceReady? | ||
* clean up / clarify properties / annotations code | ||
* drop initializeObservableProperty/ies | ||
* introduce .props(target, prop, value), .props(props), .props(target, ...props) | ||
* use disposable like RxJs? | ||
* ~~License~~ | ||
* ~~implement array.sort & reverse properly, they do change the array~~ | ||
* ~~drop mobservable.onReady / onceReady?~~ | ||
* ~~clean up / clarify properties / annotations code~~ | ||
* ~~drop initializeObservableProperties -> turnObservablesIntoProperties ~~ | ||
* ~~introduce .props(target, prop, value), .props(props), .props(target, ...props)~~ | ||
* ~~use disposable like RxJs? -> NO ~~ | ||
* ~~props should create real observable array~~ | ||
* ~~value for array, + set -> replace~~ | ||
* ~~check nested watches! inner watch should not reevaluate outer watch, introduce DNode.unobserved that swaps out trackingstack?~~ | ||
* ~~rename .prop. -> .impl.~~ | ||
* ~~replace instanceof observableValue checks with isObservable, isWrappedObservable~~ | ||
* ~~introduce createGetterSetter on all implementations~~ | ||
* ~~make properties non-configurable~~ | ||
* ~~helpers .variable, computed, array~~ | ||
* ~~introduce IGetter / ISetter interfaces, or create getter setter for array~~ | ||
* ~~perf tests~~ | ||
* ~~test .value etc~~ | ||
* ~~removed .bind, it is slow!~~ | ||
* ~~watchClass~~ | ||
* ~~move out scheduler check stuff..~~ | ||
* ~~check batching~~ | ||
* ~~check closure usages for scheduler.schedule~~ | ||
* ~~no source map for dist!~~ | ||
* fiddle demo | ||
* update apidocs | ||
0.5 | ||
* ~~browser based tests~~ | ||
* ~~fix instanceof array~~ | ||
* ~~implement + test toPlainValue~~ | ||
* ~~make browser / amd / umd build https://github.com/bebraw/grunt-umd https://github.com/umdjs/umd~~ | ||
* ~~minify~~ | ||
* react mixin, https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750 | ||
* react fiddle demo | ||
* mobservable.struct (ALWAYS compare deep equal, use defensive copy so that changes in object are detected) | ||
* mobservable.computedStruct | ||
Later | ||
* coverage tests | ||
* minified version | ||
* add 'tap' function (that doesn't register as new observer) | ||
* add 'name' as parameter to observable.value, automatically set it when defining properties, use it in warnings / toString | ||
* browser support test https://ci.testling.com/ | ||
* Introduce mobservable.object(data) that creates observable properties and an observe method. | ||
* react components | ||
* should arrays trigger a notify observed for methods like reverse, sort, values, json etc? -> they change the internal structure, so that seems to be weird | ||
0.5 | ||
* nested watcher test |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1478105
18
2332
484
9
1