tomjs
Version:
tomjs framework
644 lines (597 loc) • 20.8 kB
JavaScript
// fork by : couralex/universal-observer
// https://github.com/couralex/universal-observer#readme
const
INSERT = 'insert',
UPDATE = 'update',
DELETE = 'delete',
REVERSE = 'reverse',
SHUFFLE = 'shuffle',
sysObsKey = Symbol('system-observer-key'),
nonObservables = {
Date: true,
Blob: true,
Number: true,
String: true,
Boolean: true,
Error: true,
SyntaxError: true,
TypeError: true,
URIError: true,
Function: true,
Promise: true,
RegExp: true
},
observableDefinition = {
revoke: {
value: function() {
this[sysObsKey].revoke();
}
},
observe: {
value: function(observer) {
let systemObserver = this[sysObsKey],
observers = systemObserver.observers;
if (systemObserver.isRevoked) { throw new TypeError('revoked Observable MAY NOT be observed anymore'); }
if (typeof observer !== 'function') { throw new Error('observer parameter MUST be a function'); }
if (observers.indexOf(observer) < 0) {
observers.push(observer);
} else {
console.info('observer may be bound to an observable only once');
}
}
},
unobserve: {
value: function() {
let systemObserver = this[sysObsKey],
observers = systemObserver.observers,
l, idx;
if (systemObserver.isRevoked) { throw new TypeError('revoked Observable MAY NOT be unobserved anymore'); }
l = arguments.length;
if (l) {
while (l--) {
idx = observers.indexOf(arguments[l]);
if (idx >= 0) observers.splice(idx, 1);
}
} else {
observers.splice(0);
}
}
}
},
prepareArray = function(origin, destination, observer) {
let l = origin.length, item;
destination[sysObsKey] = observer;
while (l--) {
destination[l] = origin[l];
// item = origin[l];
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// destination[l] = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: l, parent: observer}).proxy
// : new ObjectObserver({target: item, ownKey: l, parent: observer}).proxy;
// } else {
// destination[l] = item;
// }
}
},
prepareObject = function(origin, destination, observer) {
let keys = Object.keys(origin), l = keys.length, key, item;
destination[sysObsKey] = observer;
while (l--) {
key = keys[l];
destination[key] = origin[key];
// item = origin[key];
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// destination[key] = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: key, parent: observer}).proxy
// : new ObjectObserver({target: item, ownKey: key, parent: observer}).proxy;
// } else {
// destination[key] = item;
// }
}
},
callObservers = async function(observers, changes) {
let l = observers.length;
while (l--) {
try {
await observers[l](changes);
} catch (e) {
console.error('failed to deliver changes to listener' + observers[l], e);
}
}
},
getAncestorInfo = function(self) {
let tmp = [], result, l1 = 0, l2 = 0;
while (self.parent) {
tmp[l1++] = self.ownKey;
self = self.parent;
}
result = new Array(l1);
while (l1--) result[l2++] = tmp[l1];
return {observers: self.observers, path: result};
};
class ArrayObserver {
constructor(properties) {
let origin = properties.target, clone = new Array(origin.length);
if (properties.parent === null) {
this.isRevoked = false;
this.observers = [];
Object.defineProperties(clone, observableDefinition);
} else {
this.parent = properties.parent;
this.ownKey = properties.ownKey;
}
prepareArray(origin, clone, this);
this.revokable = Proxy.revocable(clone, this);
this.proxy = this.revokable.proxy;
this.target = clone;
}
// returns an unobserved graph (effectively this is an opposite of an ArrayObserver constructor logic)
revoke() {
// revoke native proxy
this.revokable.revoke();
// roll back observed array to an unobserved one
let target = this.target, l = target.length, item;
while (l--) {
item = target[l];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
target[l] = tmpObserved.revoke();
}
}
}
return target;
}
get(target, key) {
const proxiedArrayMethods = {
pop: async function proxiedPop(target, observed) {
let poppedIndex, popResult;
poppedIndex = target.length - 1;
popResult = target.pop();
if (popResult && typeof popResult === 'object') {
let tmpObserved = popResult[sysObsKey];
if (tmpObserved) {
popResult = tmpObserved.revoke();
}
}
// publish changes
let ad = getAncestorInfo(observed);
if (ad.observers.length) {
ad.path.push(poppedIndex);
await callObservers(ad.observers, [{type: DELETE, path: ad.path, oldValue: popResult}]);
}
return popResult;
},
push: async function proxiedPush(target, observed) {
let i, l = arguments.length - 2, item, pushContent = new Array(l), pushResult, changes,
initialLength, ad = getAncestorInfo(observed);
initialLength = target.length;
for (i = 0; i < l; i++) {
item = arguments[i + 2];
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// item = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: initialLength + i, parent: observed}).proxy
// : new ObjectObserver({target: item, ownKey: initialLength + i, parent: observed}).proxy;
// }
pushContent[i] = item;
}
pushResult = Reflect.apply(target.push, target, pushContent);
// publish changes
if (ad.observers.length) {
changes = [];
for (i = initialLength, l = target.length; i < l; i++) {
let path = ad.path.slice(0);
path.push(i);
changes[i - initialLength] = {type: INSERT, path: path, value: target[i]};
}
await callObservers(ad.observers, changes);
}
return pushResult;
},
shift: async function proxiedShift(target, observed) {
let shiftResult, i, l, item, ad, changes;
shiftResult = target.shift();
if (shiftResult && typeof shiftResult === 'object') {
let tmpObserved = shiftResult[sysObsKey];
if (tmpObserved) {
shiftResult = tmpObserved.revoke();
}
}
// update indices of the remaining items
for (i = 0, l = target.length; i < l; i++) {
item = target[i];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
tmpObserved.ownKey = i;
}
}
}
// publish changes
ad = getAncestorInfo(observed);
if (ad.observers.length) {
ad.path.push(0);
changes = [{type: DELETE, path: ad.path, oldValue: shiftResult}];
await callObservers(ad.observers, changes);
}
return shiftResult;
},
unshift: async function proxiedUnshift(target, observed) {
let unshiftContent, unshiftResult, ad, changes;
unshiftContent = Array.from(arguments);
unshiftContent.splice(0, 2);
// unshiftContent.forEach((item, index) => {
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// unshiftContent[index] = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: index, parent: observed}).proxy
// : new ObjectObserver({target: item, ownKey: index, parent: observed}).proxy;
// }
// });
unshiftResult = Reflect.apply(target.unshift, target, unshiftContent);
for (let i = 0, l = target.length, item; i < l; i++) {
item = target[i];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
tmpObserved.ownKey = i;
}
}
}
// publish changes
ad = getAncestorInfo(observed);
if (ad.observers.length) {
let l = unshiftContent.length, path;
changes = new Array(l);
for (let i = 0; i < l; i++) {
path = ad.path.slice(0);
path.push(i);
changes[i] = {type: INSERT, path: path, value: target[i]};
}
await callObservers(ad.observers, changes);
}
return unshiftResult;
},
reverse: async function proxiedReverse(target, observed) {
let i, l, item, ad, changes;
target.reverse();
for (i = 0, l = target.length; i < l; i++) {
item = target[i];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
tmpObserved.ownKey = i;
}
}
}
// publish changes
ad = getAncestorInfo(observed);
if (ad.observers.length) {
changes = [{type: REVERSE, path: ad.path}];
await callObservers(ad.observers, changes);
}
return observed.proxy;
},
sort: async function proxiedSort(target, observed, comparator) {
let i, l, item, ad, changes;
target.sort(comparator);
for (i = 0, l = target.length; i < l; i++) {
item = target[i];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
tmpObserved.ownKey = i;
}
}
}
// publish changes
ad = getAncestorInfo(observed);
if (ad.observers.length) {
changes = [{type: SHUFFLE, path: ad.path}];
await callObservers(ad.observers, changes);
}
return observed.proxy;
},
fill: async function proxiedFill(target, observed) {
let ad = getAncestorInfo(observed), normArgs, argLen,
start, end, changes = [], prev, tarLen = target.length, path;
normArgs = Array.from(arguments);
normArgs.splice(0, 2);
argLen = normArgs.length;
start = argLen < 2 ? 0 : (normArgs[1] < 0 ? tarLen + normArgs[1] : normArgs[1]);
end = argLen < 3 ? tarLen : (normArgs[2] < 0 ? tarLen + normArgs[2] : normArgs[2]);
prev = target.slice(0);
Reflect.apply(target.fill, target, normArgs);
for (let i = start, item, tmpTarget; i < end; i++) {
item = target[i];
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// target[i] = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: i, parent: observed}).proxy
// : new ObjectObserver({target: item, ownKey: i, parent: observed}).proxy;
// }
if (prev.hasOwnProperty(i)) {
tmpTarget = prev[i];
if (tmpTarget && typeof tmpTarget === 'object') {
let tmpObserved = tmpTarget[sysObsKey];
if (tmpObserved) {
tmpTarget = tmpObserved.revoke();
}
}
path = ad.path.slice(0);
path.push(i);
changes.push({type: UPDATE, path: path, value: target[i], oldValue: tmpTarget});
} else {
path = ad.path.slice(0);
path.push(i);
changes.push({type: INSERT, path: path, value: target[i]});
}
}
// publish changes
if (ad.observers.length) {
await callObservers(ad.observers, changes);
}
return observed.proxy;
},
splice: async function proxiedSplice(target, observed) {
let ad = getAncestorInfo(observed),
spliceContent, spliceResult, changes = [], tmpObserved,
startIndex, removed, inserted, splLen, tarLen = target.length;
spliceContent = Array.from(arguments);
spliceContent.splice(0, 2);
splLen = spliceContent.length;
// observify the newcomers
// for (let i = 2, item; i < splLen; i++) {
// item = spliceContent[i];
// if (item && typeof item === 'object' && !nonObservables.hasOwnProperty(item.constructor.name)) {
// spliceContent[i] = Array.isArray(item)
// ? new ArrayObserver({target: item, ownKey: i, parent: observed}).proxy
// : new ObjectObserver({target: item, ownKey: i, parent: observed}).proxy;
// }
// }
// calculate pointers
startIndex = splLen === 0 ? 0 : (spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0]);
removed = splLen < 2 ? tarLen - startIndex : spliceContent[1];
inserted = Math.max(splLen - 2, 0);
spliceResult = Reflect.apply(target.splice, target, spliceContent);
tarLen = target.length;
// reindex the paths
for (let i = 0, item; i < tarLen; i++) {
item = target[i];
if (item && typeof item === 'object') {
tmpObserved = item[sysObsKey];
if (tmpObserved) {
tmpObserved.ownKey = i;
}
}
}
// revoke removed Observed
let i, l, item;
for (i = 0, l = spliceResult.length; i < l; i++) {
item = spliceResult[i];
if (item && typeof item === 'object') {
tmpObserved = item[sysObsKey];
if (tmpObserved) {
spliceResult[i] = tmpObserved.revoke();
}
}
}
// publish changes
if (ad.observers.length) {
let index, path;
for (index = 0; index < removed; index++) {
path = ad.path.slice(0);
path.push(startIndex + index);
if (index < inserted) {
changes.push({
type: UPDATE,
path: path,
value: target[startIndex + index],
oldValue: spliceResult[index]
});
} else {
changes.push({type: DELETE, path: path, oldValue: spliceResult[index]});
}
}
for (; index < inserted; index++) {
path = ad.path.slice(0);
path.push(startIndex + index);
changes.push({type: INSERT, path: path, value: target[startIndex + index]});
}
await callObservers(ad.observers, changes);
}
return spliceResult;
},
set:async function set(target, observed, key, value) {
let oldValue = target[key], ad, changes;
// if (value && typeof value === 'object' && !nonObservables.hasOwnProperty(value.constructor.name)) {
// target[key] = Array.isArray(value)
// ? new ArrayObserver({target: value, ownKey: key, parent: this}).proxy
// : new ObjectObserver({target: value, ownKey: key, parent: this}).proxy;
// } else {
// target[key] = value;
// }
target[key] = value;
if (oldValue && typeof oldValue === 'object') {
let tmpObserved = oldValue[sysObsKey];
if (tmpObserved) {
oldValue = tmpObserved.revoke();
}
}
// publish changes
ad = getAncestorInfo(observed);
if (ad.observers.length) {
ad.path.push(key);
changes = typeof oldValue === 'undefined'
? [{type: INSERT, path: ad.path, value: value}]
: [{type: UPDATE, path: ad.path, value: value, oldValue: oldValue}];
await callObservers(ad.observers, changes);
}
return true;
}
};
if (proxiedArrayMethods.hasOwnProperty(key)) {
return proxiedArrayMethods[key].bind(undefined, target, this);
} else {
return target[key];
}
}
async set(target, key, value) {
return false;
// let oldValue = target[key], ad, changes;
// // if (value && typeof value === 'object' && !nonObservables.hasOwnProperty(value.constructor.name)) {
// // target[key] = Array.isArray(value)
// // ? new ArrayObserver({target: value, ownKey: key, parent: this}).proxy
// // : new ObjectObserver({target: value, ownKey: key, parent: this}).proxy;
// // } else {
// // target[key] = value;
// // }
// target[key] = value;
// if (oldValue && typeof oldValue === 'object') {
// let tmpObserved = oldValue[sysObsKey];
// if (tmpObserved) {
// oldValue = tmpObserved.revoke();
// }
// }
// // publish changes
// ad = getAncestorInfo(this);
// if (ad.observers.length) {
// ad.path.push(key);
// changes = typeof oldValue === 'undefined'
// ? [{type: INSERT, path: ad.path, value: value}]
// : [{type: UPDATE, path: ad.path, value: value, oldValue: oldValue}];
// await callObservers(ad.observers, changes);
// }
// return true;
}
async deleteProperty(target, key) {
let oldValue = target[key], ad, changes;
if (delete target[key]) {
if (oldValue && typeof oldValue === 'object') {
let tmpObserved = oldValue[sysObsKey];
if (tmpObserved) {
oldValue = tmpObserved.revoke();
}
}
// publish changes
ad = getAncestorInfo(this);
if (ad.observers.length) {
ad.path.push(key);
changes = [{type: DELETE, path: ad.path, oldValue: oldValue}];
await callObservers(ad.observers, changes);
}
return true;
} else {
return false;
}
}
}
class ObjectObserver {
constructor(properties) {
let origin = properties.target, clone = {};
if (properties.parent === null) {
this.isRevoked = false;
this.observers = [];
Object.defineProperties(clone, observableDefinition);
} else {
this.parent = properties.parent;
this.ownKey = properties.ownKey;
}
prepareObject(origin, clone, this);
this.revokable = Proxy.revocable(clone, this);
this.proxy = this.revokable.proxy;
this.target = clone;
}
// returns an unobserved graph (effectively this is an opposite of an ObjectObserver constructor logic)
revoke() {
// revoke native proxy
this.revokable.revoke();
// roll back observed graph to an unobserved one
let target = this.target, keys = Object.keys(target), l = keys.length, key, item;
while (l--) {
key = keys[l];
item = target[key];
if (item && typeof item === 'object') {
let tmpObserved = item[sysObsKey];
if (tmpObserved) {
target[key] = tmpObserved.revoke();
}
}
}
return target;
}
async set(target, key, value) {
let oldValue = target[key], ad, changes;
if (value && typeof value === 'object' && !nonObservables.hasOwnProperty(value.constructor.name)) {
target[key] = Array.isArray(value)
? new ArrayObserver({target: value, ownKey: key, parent: this}).proxy
: new ObjectObserver({target: value, ownKey: key, parent: this}).proxy;
} else {
target[key] = value;
}
if (oldValue && typeof oldValue === 'object') {
let tmpObserved = oldValue[sysObsKey];
if (tmpObserved) {
oldValue = tmpObserved.revoke();
}
}
// publish changes
ad = getAncestorInfo(this);
if (ad.observers.length) {
ad.path.push(key);
changes = typeof oldValue === 'undefined'
? [{type: INSERT, path: ad.path, value: value}]
: [{type: UPDATE, path: ad.path, value: value, oldValue: oldValue}];
await callObservers(ad.observers, changes);
}
return true;
}
async deleteProperty(target, key) {
let oldValue = target[key], ad, changes;
if (delete target[key]) {
if (oldValue && typeof oldValue === 'object') {
let tmpObserved = oldValue[sysObsKey];
if (tmpObserved) {
oldValue = tmpObserved.revoke();
}
}
// publish changes
ad = getAncestorInfo(this);
if (ad.observers.length) {
ad.path.push(key);
changes = [{type: DELETE, path: ad.path, oldValue: oldValue}];
await callObservers(ad.observers, changes);
}
return true;
} else {
return false;
}
}
}
class Observable {
constructor() {
throw new Error('Observable MAY NOT be created via constructor, see "Observable.from" API');
}
static from(target) {
if (target && typeof target === 'object' && !nonObservables.hasOwnProperty(target.constructor.name) && !('observe' in target) && !('unobserve' in target) && !('revoke' in target)) {
let observed = Array.isArray(target)
? new ArrayObserver({target: target, ownKey: null, parent: null})
: new ObjectObserver({target: target, ownKey: null, parent: null});
return observed.proxy;
} else {
if (!target || typeof target !== 'object') {
throw new Error('observable MAY ONLY be created from non-null object only');
} else if ('observe' in target || 'unobserve' in target || 'revoke' in target) {
throw new Error('target object MUST NOT have nor own neither inherited properties from the following list: "observe", "unobserve", "revoke"');
} else if (nonObservables.hasOwnProperty(target.constructor.name)) {
throw new Error(target + ' found to be one of non-observable object types: ' + nonObservables);
}
}
}
static isObservable(input) {
return typeof input === 'object' && input !== null &&
typeof input.revoke === 'function' &&
typeof input.observe === 'function' &&
typeof input.unobserve === 'function';
}
}
Object.freeze(Observable);
module.exports = Observable;