Proposal Replace object.observe by Kefir.Stream or the IBM Shim
frank-dspeed opened this issue ยท 1 comments
frank-dspeed commented
lets use Kefir Streams ๐
Or the IBM Code
This Closes: #60 #42
Source from IBM decor
IBM decor Shim for OberservableArray
/** @module liaison/ObservableArray */
define([
"requirejs-dplugins/has",
"./Observable"
], function (has, Observable) {
"use strict";
/**
* The same argument list of Array, taking the length of the new array or the initial list of array elements.
* @typedef {number|...Anything} module:liaison/ObservableArray~CtorArguments
*/
/**
* An observable array, working as a shim
* of {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @class
* @alias module:liaison/ObservableArray
* @augments module:decor/Observable
* @param {module:decor/ObservableArray~CtorArguments} [args]
* The length of the new array or the initial list of array elements.
*/
var ObservableArray,
augmentedMethods,
defineProperty = Object.defineProperty,
EMPTY_ARRAY = [],
REGEXP_GLOBAL_OBJECT = /\[\s*object\s+global\s*\]/i; // Global object in node.js
(function () {
var observableArrayMarker = "_observableArray";
if (has("object-observe-api")) {
// For useNative case, make ObservableArray an instance of Array instead of an inheritance,
// so that Array.observe() emits splices for .length update
ObservableArray = function (length) {
var self = [];
Observable.call(self);
// Make ObservableArray marker not enumerable, configurable or writable
defineProperty(self, observableArrayMarker, {value: 1});
defineProperty(self, "set", Object.getOwnPropertyDescriptor(Observable.prototype, "set"));
if (typeof length === "number" && arguments.length === 1) {
self.length = length;
} else {
EMPTY_ARRAY.push.apply(self, arguments);
}
return self;
};
} else {
// TODO(asudoh):
// Document that ObservableArray cannot be observed by Observable.observe()
// without "splice" in accept list.
// We need to create large amount of change records to do so,
// when splice happens with large amount of removals/adds
ObservableArray = function (length) {
var beingConstructed = this && !REGEXP_GLOBAL_OBJECT.test(this) && !this.hasOwnProperty("length"),
// If this is called as regular function (instead of constructor), work with a new instance
self = beingConstructed ? [] : new ObservableArray();
if (beingConstructed) {
Observable.call(self);
// Make ObservableArray marker not enumerable, configurable or writable
defineProperty(self, observableArrayMarker, {value: 1});
// Make those methods not enumerable
for (var s in augmentedMethods) {
defineProperty(self, s, {
value: augmentedMethods[s],
configurable: true,
writable: true
});
}
}
if (typeof length === "number" && arguments.length === 1) {
self.length = length;
} else {
EMPTY_ARRAY.push.apply(self, arguments);
}
return self;
};
}
/**
* @method module:liaison/ObservableArray.test
* @param {Array} a The array to test.
* @returns {boolean} true if o is an instance of {@link module:liaison/ObservableArray ObservableArray}.
*/
ObservableArray.test = function (a) {
return a && a[observableArrayMarker];
};
})();
/**
* @method module:liaison/ObservableArray.canObserve
* @param {Array} a The array to test.
* @returns {boolean}
* true if o can be observed with {@link module:liaison/ObservableArray.observe ObservableArray.observe()}.
*/
if (has("object-observe-api")) {
ObservableArray.canObserve = function (a) {
return typeof (a || {}).splice === "function";
};
} else {
ObservableArray.canObserve = ObservableArray.test;
}
if (!has("object-observe-api")) {
(function () {
/**
* Adds and/or removes elements from an array
* and automatically emits a change record compatible
* with {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @param {number} index Index at which to start changing the array.
* @param {number} removeCount [An integer indicating the number of old array elements to remove.
* @param {...Anything} [var_args] The elements to add to the array.
* @return {Array} An array containing the removed elements.
* @memberof module:liaison/ObservableArray#
*/
function splice(index, removeCount) {
/* jshint validthis: true */
if (index < 0) {
index = this.length + index;
}
var oldLength = this.length,
changeRecord = {
index: index,
removed: this.slice(index, index + removeCount),
addedCount: arguments.length - 2
},
result = EMPTY_ARRAY.splice.apply(this, arguments),
lengthRecord = oldLength !== this.length && {
type: "update",
object: this,
name: "length",
oldValue: oldLength
},
notifier = Observable.getNotifier(this);
notifier.performChange("splice", function () {
lengthRecord && notifier.notify(lengthRecord);
return changeRecord;
});
return result;
}
augmentedMethods = /** @lends module:liaison/ObservableArray# */ {
splice: splice,
/**
* Sets a value and automatically emits change record(s)
* compatible with
* {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @param {string} name The property name.
* @param value The property value.
* @returns The value set.
*/
set: function (name, value) {
var args;
if (name === "length") {
args = new Array(Math.max(value - this.length, 0));
args.unshift(Math.min(this.length, value), Math.max(this.length - value, 0));
splice.apply(this, args);
} else if (!isNaN(name) && +name >= this.length) {
args = new Array(name - this.length);
args.push(value);
args.unshift(this.length, 0);
splice.apply(this, args);
} else {
Observable.prototype.set.call(this, name, value);
}
return value;
},
/**
* Removes the last element from an array
* and automatically emits a change record compatible with
* {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @returns The element removed.
*/
pop: function () {
return splice.call(this, -1, 1)[0];
},
/**
* Adds one or more elements to the end of an array
* and automatically emits a change record compatible with
* {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @param {...Anything} var_args The elements to add to the end of the array.
* @returns The new length of the array.
*/
push: function () {
var args = [this.length, 0];
EMPTY_ARRAY.push.apply(args, arguments);
splice.apply(this, args);
return this.length;
},
/**
* Reverses the order of the elements of an array
* and automatically emits a splice type of change record.
* @returns {Array} The array itself.
*/
reverse: function () {
var changeRecord = {
type: "splice",
object: this,
index: 0,
removed: this.slice(),
addedCount: this.length
},
result = EMPTY_ARRAY.reverse.apply(this, arguments);
// Treat this change as a splice instead of updates in each entry
Observable.getNotifier(this).notify(changeRecord);
return result;
},
/**
* Removes the first element from an array
* and automatically emits a change record compatible with
* {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @returns The element removed.
*/
shift: function () {
return splice.call(this, 0, 1)[0];
},
/**
* Sorts the elements of an array in place
* and automatically emits a splice type of change record.
* @returns {Array} The array itself.
*/
sort: function () {
var changeRecord = {
type: "splice",
object: this,
index: 0,
removed: this.slice(),
addedCount: this.length
},
result = EMPTY_ARRAY.sort.apply(this, arguments);
// Treat this change as a splice instead of updates in each entry
Observable.getNotifier(this).notify(changeRecord);
return result;
},
/**
* Adds one or more elements to the front of an array
* and automatically emits a change record compatible with
* {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Array.observe()}.
* @param {...Anything} var_args The elements to add to the front of the array.
* @returns The new length of the array.
*/
unshift: function () {
var args = [0, 0];
EMPTY_ARRAY.push.apply(args, arguments);
splice.apply(this, args);
return this.length;
}
};
})();
}
/**
* Observes an ObservableArray for changes.
* Internally calls {@link module:decor/Observable.observe Observable.observe()}
* observing for the following types of change records:
* [
* "add",
* "update",
* "delete",
* "splice"
* ]
* All change records will be converted to "splice" and are sorted by index and merged to smaller number
* of change records.
* @method
* @param {Object} observable The {@link module:liaison/ObservableArray ObservableArray} to observe.
* @param {module:decor/Observable~ChangeCallback} callback The change callback.
* @returns {Handle} The handle to stop observing.
* @throws {TypeError} If the 1st argument is non-object or null.
*/
ObservableArray.observe = (function () {
function intersect(start1, end1, start2, end2) {
return end1 <= start2 ? end1 - start2 : // Adjacent or distant
end2 <= start1 ? end2 - start1 : // Adjacent or distant
Math.min(end1, end2) - Math.max(start1, start2); // Intersected or contained
}
function normalize(record) {
return record.type !== "add" && record.type !== "update" ? record :
{
type: "splice",
object: record.object,
index: +record.name,
removed: [record.oldValue],
addedCount: 1
};
}
function observeSpliceCallback(callback, records) {
var merged = [];
records.forEach(function (incoming) {
incoming = normalize(incoming);
var doneIncoming = false,
indexAdjustment = 0;
for (var i = 0; i < merged.length; ++i) {
var entry;
if (!has("object-observe-api") || !Object.isFrozen(merged[i])) {
entry = merged[i];
entry.index += indexAdjustment;
} else {
entry = merged[i] = {
type: "splice",
object: merged[i].object,
index: merged[i].index + indexAdjustment,
removed: merged[i].removed,
addedCount: merged[i].addedCount
};
}
/* jshint maxlen:150 */
var amount = intersect(entry.index, entry.index + entry.addedCount, incoming.index, incoming.index + incoming.removed.length);
if (amount >= 0) {
// Merge splices
merged.splice(i--, 1);
var removed,
addedCount = entry.addedCount - amount + incoming.addedCount;
if (entry.index < incoming.index) {
removed = incoming.removed.slice(Math.max(amount, 0));
EMPTY_ARRAY.unshift.apply(removed, entry.removed);
} else {
removed = incoming.removed.slice(0, amount > 0 ? entry.index - incoming.index : incoming.length);
EMPTY_ARRAY.push.apply(removed, entry.removed);
// Append happens when second splice's range contains first splice's range
EMPTY_ARRAY.push.apply(removed, incoming.removed.slice(entry.index + entry.addedCount - incoming.index));
}
/* jshint maxlen:120 */
if (removed.length === 0 && addedCount === 0) {
doneIncoming = true;
} else {
incoming = {
type: "splice",
object: entry.object,
index: Math.min(entry.index, incoming.index),
removed: removed,
addedCount: addedCount
};
}
indexAdjustment -= entry.addedCount - entry.removed.length; // entry is subsumed by incoming
} else if (incoming.index < entry.index) {
// Insert the new splice
var adjustment = incoming.addedCount - incoming.removed.length;
entry.index += adjustment;
indexAdjustment += adjustment;
merged.splice(i++, 0, incoming);
doneIncoming = true;
}
}
if (!doneIncoming) {
merged.push(incoming);
}
});
if (merged.length > 0) {
callback(merged);
}
}
if (has("object-observe-api")) {
return function (observableArray, callback) {
Array.observe(observableArray, callback = observeSpliceCallback.bind(observableArray, callback));
return {
deliver: Object.deliverChangeRecords.bind(Object, callback),
remove: Array.unobserve.bind(Array, observableArray, callback)
};
};
} else {
return function (observableArray, callback) {
var h = Object.create(Observable.observe(observableArray,
callback = observeSpliceCallback.bind(observableArray, callback), [
"add",
"update",
"delete",
"splice"
]));
h.deliver = Observable.deliverChangeRecords.bind(Observable, callback);
return h;
};
}
})();
return ObservableArray;
});
IBM Decor Shim for Observeable
/** @module decor/Observable */
define([
"./features",
"./features!object-observe-api?:./schedule"
], function (has, schedule) {
"use strict";
/**
* An observable object, working as a shim
* of {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe ECMAScript Harmony Object.observe()}.
* @class
* @alias module:decor/Observable
* @param {Object} o The object to mix-into the new Observable.
* @example
* var observable = new Observable({foo: "Foo0"});
* Observable.observe(observable, function (changeRecords) {
* // Called at the end of microtask with:
* // [
* // {
* // type: "update",
* // object: observable,
* // name: "foo",
* // oldValue: "Foo0"
* // },
* // {
* // type: "add",
* // object: observable,
* // name: "bar"
* // }
* // ]
* });
* observable.set("foo", "Foo1");
* observable.set("bar", "Bar0");
*/
var Observable,
defineProperty = Object.defineProperty,
getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
/**
* The default list of change record types, which is:
* [
* "add",
* "update",
* "delete",
* "reconfigure",
* "setPrototype",
* "preventExtensions"
* ]
* @constant {Array.<module:decor/Observable~ChangeType>}
* module:decor/Observable~DEFAULT_CHANGETYPES
*/
var DEFAULT_ACCEPT_CHANGETYPES = {
"add": 1,
"update": 1,
"delete": 1,
"reconfigure": 1,
"setPrototype": 1,
"preventExtensions": 1
}; // Observable#set() only supports the first two
/**
* Change record type.
* One of:
* * "add"
* * "update"
* * "delete"
* * "reconfigure"
* * "setPrototype"
* * "preventExtensions"
* * "splice"
* @typedef {string} module:decor/Observable~ChangeType
*/
/**
* Change record seen in Observable.observe().
* @typedef {Object} module:decor/Observable~ChangeRecord
* @property {module:decor/Observable~ChangeType} type The type of change record.
* @property {Object} object The changed object.
* @property {string} [name] The changed property name. Set only for non-splice type of change records.
* @property {number} [index] The array index of splice. Set only for splice type of change records.
* @property {Array} [removed] The removed array elements. Set only for splice type of change records.
* @property {number} [addedCount] The count of added array elements. Set only for splice type of change records.
*/
/**
* Change callback.
* @callback module:decor/Observable~ChangeCallback
* @param {Array.<module:decor/Observable~ChangeRecord>} changeRecords The change records.
*/
Observable = function (o) {
// Make Observable marker not enumerable, configurable or writable
if (!this._observable) { // In case this constructor is called manually
defineProperty(this, "_observable", {value: 1});
}
o && Observable.assign(this, o);
};
/**
* @method module:decor/Observable.test
* @param {Object} o The object to test.
* @returns {boolean} true if o is an instance of Observable.
*/
Observable.test = function (o) {
return o && o._observable;
};
/**
* @method module:decor/Observable.is
* @returns {boolean} true if the given two values are the same, considering NaN as well as +0 vs. -0.
*/
Observable.is = has("object-is-api") ? Object.is : function (lhs, rhs) {
return lhs === rhs && (lhs !== 0 || 1 / lhs === 1 / rhs) || lhs !== lhs && rhs !== rhs;
};
/**
* Copy properties of given source objects to given target object.
* If target object has {@link module:decor/Observable#set set()} function for the property, uses it.
* @function module:decor/Observable.assign
* @param {Object} dst The target object.
* @param {...Object} var_args The source objects.
* @returns {Object} The target object.
*/
Observable.assign = function (dst) {
if (dst == null) {
throw new TypeError("Can't convert " + dst + " to object.");
}
dst = Object(dst);
for (var i = 1, l = arguments.length; i < l; ++i) {
var src = Object(arguments[i]),
props = Object.getOwnPropertyNames(src);
for (var j = 0, m = props.length; j < m; ++j) {
var prop = props[j];
Observable.prototype.set.call(dst, prop, src[prop]);
}
}
return dst;
};
/**
* @method module:decor/Observable.canObserve
* @param {Object} o The object to test.
* @returns {boolean} true if o can be observed with {@link module:decor/Observable.observe Observable.observe()}.
*/
if (has("object-observe-api")) {
Observable.canObserve = function (o) {
return typeof o === "object" && o != null;
};
} else {
Observable.canObserve = Observable.test;
}
if (has("object-observe-api")) {
defineProperty(Observable.prototype, "set", { // Make set() not enumerable
value: function (name, value) {
this[name] = value;
return value;
},
configurable: true,
writable: true
});
Observable.observe = function (object, callback, accept) {
Object.observe.call(this, object, callback, accept);
return {
remove: function () {
Object.unobserve(object, callback);
}
};
};
Observable.getNotifier = Object.getNotifier;
Observable.deliverChangeRecords = Object.deliverChangeRecords;
} else {
defineProperty(Observable.prototype, "set", { // Make set() not enumerable
/**
* Sets a value.
* Automatically emits change record(s)
* compatible with {@link http://wiki.ecmascript.org/doku.php?id=harmony:observe Object.observe()}
* if no ECMAScript setter is defined for the given property.
* If ECMAScript setter is defined for the given property, use
* {@link module:decor/Observable~Notifier#notify Observable.getNotifier(observable).notify(changeRecord)}
* to manually emit a change record.
* @method module:decor/Observable#set
* @param {string} name The property name.
* @param value The property value.
* @returns The value set.
*/
value: function (name, value) {
var type = name in this ? "update" : "add",
oldValue = this[name],
// For defining setter, ECMAScript setter should be used
setter = (getOwnPropertyDescriptor(this, name) || {}).set;
this[name] = value;
if (!Observable.is(value, oldValue) && setter === undefined) {
// Auto-notify if there is no setter defined for the property.
// Application should manually call Observable.getNotifier(observable).notify(changeRecord)
// if a setter is defined.
var changeRecord = {
type: type,
object: this,
name: name + ""
};
if (type === "update") {
changeRecord.oldValue = oldValue;
}
Observable.getNotifier(this).notify(changeRecord);
}
return value;
},
configurable: true,
writable: true
});
var seq = 0,
hotCallbacks = {},
deliverHandle = null,
deliverAllByTimeout = function () {
/* global Platform */
has("polymer-platform") && Platform.performMicrotaskCheckpoint(); // For Polymer watching for Observable
for (var anyWorkDone = true; anyWorkDone;) {
anyWorkDone = false;
// Observation may stop during observer callback
var callbacks = [];
for (var s in hotCallbacks) {
callbacks.push(hotCallbacks[s]);
}
hotCallbacks = {};
callbacks = callbacks.sort(function (lhs, rhs) {
return lhs._seq - rhs._seq;
});
for (var i = 0, l = callbacks.length; i < l; ++i) {
if (callbacks[i]._changeRecords.length > 0) {
Observable.deliverChangeRecords(callbacks[i]);
anyWorkDone = true;
}
}
}
deliverHandle = null;
},
removeGarbageCallback = function (callback) {
if (callback._changeRecords.length === 0 && callback._refCountOfNotifier === 0) {
callback._seq = undefined;
}
};
/**
* Notifier object for Observable.
* This is an internal function and cannot be used directly.
* @class module:decor/Observable~Notifier
*/
var Notifier = function (target) {
this.target = target;
this.observers = {};
this._activeChanges = {};
};
Notifier.prototype = /** @lends module:decor/Observable~Notifier */ {
/**
* Queue up a change record.
* It will be notified at the end of microtask,
* or when {@link module:decor/Observable.deliverChangeRecords Observable.deliverChangeRecords()}
* is called.
* @method module:decor/Observable~Notifier#notify
* @param {module:decor/Observable~ChangeRecord} changeRecord
* The change record to queue up for notification.
*/
notify: function (changeRecord) {
function shouldDeliver(activeChanges, acceptTable, changeType) {
if (changeType in acceptTable) {
for (var s in acceptTable) {
if (activeChanges[s] > 0) {
return false;
}
}
return true;
}
}
for (var s in this.observers) {
if (shouldDeliver(this._activeChanges, this.observers[s].acceptTable, changeRecord.type)) {
var callback = this.observers[s].callback;
callback._changeRecords.push(changeRecord);
hotCallbacks[callback._seq] = callback;
if (!deliverHandle) {
deliverHandle = schedule(deliverAllByTimeout);
}
}
}
},
/**
* Let the series of changes made in the given callback be represented
* by a synthetic change of the given change type.
* The callback may return the synthetic change record,
* which will be of the `type` and automatically emitted.
* Otherwise, the caller can emit the synthetic record manually
* via {@link module:decor/Observable~Notifier#notify notify()}.
* @param {string} type The change type of synthetic change record.
* @param {Function} callback The callback function.
*/
performChange: function (type, callback) {
this._activeChanges[type] = (this._activeChanges[type] || 0) + 1;
var source = callback.call(undefined);
--this._activeChanges[type];
if (source) {
var target = {
type: type,
object: this.target
};
for (var s in source) {
if (!(s in target)) {
target[s] = source[s];
}
}
this.notify(target);
}
}
};
/**
* Obtains a notifier object for the given {@link module:decor/Observable Observable}.
* @method module:decor/Observable.getNotifier
* @param {Object} observable The {@link module:decor/Observable Observable} to get a notifier object of.
* @returns {module:decor/Observable~Notifier}
*/
Observable.getNotifier = function (observable) {
if (!getOwnPropertyDescriptor(observable, "_notifier")) {
// Make the notifier reference not enumerable, configurable or writable
defineProperty(observable, "_notifier", {
value: new Notifier(observable)
});
}
return observable._notifier;
};
/**
* Observes an {@link module:decor/Observable Observable} for changes.
* @method module:decor/Observable.observe
* @param {Object} observable The {@link module:decor/Observable Observable} to observe.
* @param {module:decor/Observable~ChangeCallback} callback The change callback.
* @param {Array.<module:decor/Observable~ChangeType>}
* [accept={@link module:decor/Observable~DEFAULT_CHANGETYPES}]
* The list of change record types to observe.
* @returns {Handle} The handle to stop observing.
* @throws {TypeError} If the 1st argument is non-object or null.
*/
Observable.observe = function (observable, callback, accept) {
if (Object(observable) !== observable) {
throw new TypeError("Observable.observe() cannot be called on non-object.");
}
if (!("_seq" in callback)) {
callback._seq = seq++;
callback._changeRecords = [];
callback._refCountOfNotifier = 0;
}
var acceptTable = accept ? accept.reduce(function (types, type) {
types[type] = 1;
return types;
}, {}) : DEFAULT_ACCEPT_CHANGETYPES,
notifier = Observable.getNotifier(observable);
if (!(callback._seq in notifier.observers)) {
notifier.observers[callback._seq] = {
acceptTable: acceptTable,
callback: callback
};
++callback._refCountOfNotifier;
} else {
notifier.observers[callback._seq].acceptTable = acceptTable;
}
return {
remove: function () {
if (callback._seq in notifier.observers) {
delete notifier.observers[callback._seq];
--callback._refCountOfNotifier;
}
}
};
};
/**
* Delivers change records immediately.
* @method module:decor/Observable.deliverChangeRecords
* @param {Function} callback The change callback to deliver change records of.
*/
Observable.deliverChangeRecords = function (callback) {
var length = callback._changeRecords.length;
try {
callback(callback._changeRecords.splice(0, length));
} catch (e) {
has("console-api") && console.error("Error occured in observer callback: " + (e.stack || e));
}
removeGarbageCallback(callback);
};
}
return Observable;
});
adamhalasz commented
Just published a new version with the ES6 Proxy which replaces object.observe which should fix this.