@nx-js/observer-util
Version:
Simple transparent reactivity with 100% language coverage. Made with ES6 Proxies.
478 lines (426 loc) • 17.2 kB
JavaScript
var connectionStore = new WeakMap();
var ITERATION_KEY = Symbol('iteration key');
function storeObservable(obj) {
// this will be used to save (obj.key -> reaction) connections later
connectionStore.set(obj, new Map());
}
function registerReactionForOperation(reaction, ref) {
var target = ref.target;
var key = ref.key;
var type = ref.type;
if (type === 'iterate') {
key = ITERATION_KEY;
}
var reactionsForObj = connectionStore.get(target);
var reactionsForKey = reactionsForObj.get(key);
if (!reactionsForKey) {
reactionsForKey = new Set();
reactionsForObj.set(key, reactionsForKey);
}
// save the fact that the key is used by the reaction during its current run
if (!reactionsForKey.has(reaction)) {
reactionsForKey.add(reaction);
reaction.cleaners.push(reactionsForKey);
}
}
function getReactionsForOperation(ref) {
var target = ref.target;
var key = ref.key;
var type = ref.type;
var reactionsForTarget = connectionStore.get(target);
var reactionsForKey = new Set();
if (type === 'clear') {
reactionsForTarget.forEach(function (_, key) {
addReactionsForKey(reactionsForKey, reactionsForTarget, key);
});
} else {
addReactionsForKey(reactionsForKey, reactionsForTarget, key);
}
if (type === 'add' || type === 'delete' || type === 'clear') {
var iterationKey = Array.isArray(target) ? 'length' : ITERATION_KEY;
addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey);
}
return reactionsForKey;
}
function addReactionsForKey(reactionsForKey, reactionsForTarget, key) {
var reactions = reactionsForTarget.get(key);
reactions && reactions.forEach(reactionsForKey.add, reactionsForKey);
}
function releaseReaction(reaction) {
if (reaction.cleaners) {
reaction.cleaners.forEach(releaseReactionKeyConnection, reaction);
}
reaction.cleaners = [];
}
function releaseReactionKeyConnection(reactionsForKey) {
reactionsForKey.delete(this);
}
// reactions can call each other and form a call stack
var reactionStack = [];
var isDebugging = false;
function runAsReaction(reaction, fn, context, args) {
// do not build reactive relations, if the reaction is unobserved
if (reaction.unobserved) {
return Reflect.apply(fn, context, args);
}
// only run the reaction if it is not already in the reaction stack
// TODO: improve this to allow explicitly recursive reactions
if (reactionStack.indexOf(reaction) === -1) {
// release the (obj -> key -> reactions) connections
// and reset the cleaner connections
releaseReaction(reaction);
try {
// set the reaction as the currently running one
// this is required so that we can create (observable.prop -> reaction) pairs in the get trap
reactionStack.push(reaction);
return Reflect.apply(fn, context, args);
} finally {
// always remove the currently running flag from the reaction when it stops execution
reactionStack.pop();
}
}
}
// register the currently running reaction to be queued again on obj.key mutations
function registerRunningReactionForOperation(operation) {
// get the current reaction from the top of the stack
var runningReaction = reactionStack[reactionStack.length - 1];
if (runningReaction) {
debugOperation(runningReaction, operation);
registerReactionForOperation(runningReaction, operation);
}
}
function queueReactionsForOperation(operation) {
// iterate and queue every reaction, which is triggered by obj.key mutation
getReactionsForOperation(operation).forEach(queueReaction, operation);
}
function queueReaction(reaction) {
debugOperation(reaction, this);
// queue the reaction for later execution or run it immediately
if (typeof reaction.scheduler === 'function') {
reaction.scheduler(reaction);
} else if (typeof reaction.scheduler === 'object') {
reaction.scheduler.add(reaction);
} else {
reaction();
}
}
function debugOperation(reaction, operation) {
if (reaction.debugger && !isDebugging) {
try {
isDebugging = true;
reaction.debugger(operation);
} finally {
isDebugging = false;
}
}
}
function hasRunningReaction() {
return reactionStack.length > 0;
}
var IS_REACTION = Symbol('is reaction');
function observe(fn, options) {
if ( options === void 0 ) options = {};
// wrap the passed function in a reaction, if it is not already one
var reaction = fn[IS_REACTION] ? fn : function reaction() {
return runAsReaction(reaction, fn, this, arguments);
};
// save the scheduler and debugger on the reaction
reaction.scheduler = options.scheduler;
reaction.debugger = options.debugger;
// save the fact that this is a reaction
reaction[IS_REACTION] = true;
// run the reaction once if it is not a lazy one
if (!options.lazy) {
reaction();
}
return reaction;
}
function unobserve(reaction) {
// do nothing, if the reaction is already unobserved
if (!reaction.unobserved) {
// indicate that the reaction should not be triggered any more
reaction.unobserved = true;
// release (obj -> key -> reaction) connections
releaseReaction(reaction);
}
// unschedule the reaction, if it is scheduled
if (typeof reaction.scheduler === 'object') {
reaction.scheduler.delete(reaction);
}
}
var proxyToRaw = new WeakMap();
var rawToProxy = new WeakMap();
var hasOwnProperty = Object.prototype.hasOwnProperty;
function findObservable(obj) {
var observableObj = rawToProxy.get(obj);
if (hasRunningReaction() && typeof obj === 'object' && obj !== null) {
if (observableObj) {
return observableObj;
}
return observable(obj);
}
return observableObj || obj;
}
function patchIterator(iterator, isEntries) {
var originalNext = iterator.next;
iterator.next = function () {
var ref = originalNext.call(iterator);
var done = ref.done;
var value = ref.value;
if (!done) {
if (isEntries) {
value[1] = findObservable(value[1]);
} else {
value = findObservable(value);
}
}
return { done: done, value: value };
};
return iterator;
}
var instrumentations = {
has: function has(key) {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, key: key, type: 'has' });
return proto.has.apply(target, arguments);
},
get: function get(key) {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, key: key, type: 'get' });
return findObservable(proto.get.apply(target, arguments));
},
add: function add(key) {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
var hadKey = proto.has.call(target, key);
// forward the operation before queueing reactions
var result = proto.add.apply(target, arguments);
if (!hadKey) {
queueReactionsForOperation({ target: target, key: key, value: key, type: 'add' });
}
return result;
},
set: function set(key, value) {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
var hadKey = proto.has.call(target, key);
var oldValue = proto.get.call(target, key);
// forward the operation before queueing reactions
var result = proto.set.apply(target, arguments);
if (!hadKey) {
queueReactionsForOperation({ target: target, key: key, value: value, type: 'add' });
} else if (value !== oldValue) {
queueReactionsForOperation({ target: target, key: key, value: value, oldValue: oldValue, type: 'set' });
}
return result;
},
delete: function delete$1(key) {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
var hadKey = proto.has.call(target, key);
var oldValue = proto.get ? proto.get.call(target, key) : undefined;
// forward the operation before queueing reactions
var result = proto.delete.apply(target, arguments);
if (hadKey) {
queueReactionsForOperation({ target: target, key: key, oldValue: oldValue, type: 'delete' });
}
return result;
},
clear: function clear() {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
var hadItems = target.size !== 0;
var oldTarget = target instanceof Map ? new Map(target) : new Set(target);
// forward the operation before queueing reactions
var result = proto.clear.apply(target, arguments);
if (hadItems) {
queueReactionsForOperation({ target: target, oldTarget: oldTarget, type: 'clear' });
}
return result;
},
forEach: function forEach(cb) {
var args = [], len = arguments.length - 1;
while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ];
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
// swap out the raw values with their observable pairs
// before passing them to the callback
var wrappedCb = function (value) {
var rest = [], len = arguments.length - 1;
while ( len-- > 0 ) rest[ len ] = arguments[ len + 1 ];
return cb.apply(void 0, [ findObservable(value) ].concat( rest ));
};
return (ref = proto.forEach).call.apply(ref, [ target, wrappedCb ].concat( args ));
var ref;
},
keys: function keys() {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
return proto.keys.apply(target, arguments);
},
values: function values() {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
var iterator = proto.values.apply(target, arguments);
return patchIterator(iterator, false);
},
entries: function entries() {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
var iterator = proto.entries.apply(target, arguments);
return patchIterator(iterator, true);
},
get size() {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
return Reflect.get(proto, 'size', target);
}
};
instrumentations[Symbol.iterator] = function () {
var target = proxyToRaw.get(this);
var proto = Reflect.getPrototypeOf(this);
registerRunningReactionForOperation({ target: target, type: 'iterate' });
var iterator = proto[Symbol.iterator].apply(target, arguments);
return patchIterator(iterator, target instanceof Map);
};
var collectionHandlers = {
get: function get(target, key, receiver) {
// instrument methods and property accessors to be reactive
target = hasOwnProperty.call(instrumentations, key) ? instrumentations : target;
return Reflect.get(target, key, receiver);
}
};
// eslint-disable-next-line
var globalObj = typeof window === 'object' ? window : Function('return this')();
// built-in object can not be wrapped by Proxies
// their methods expect the object instance as the 'this' instead of the Proxy wrapper
// complex objects are wrapped with a Proxy of instrumented methods
// which switch the proxy to the raw object and to add reactive wiring
var handlers = new Map([[Map, collectionHandlers], [Set, collectionHandlers], [WeakMap, collectionHandlers], [WeakSet, collectionHandlers], [Object, false], [Array, false], [Int8Array, false], [Uint8Array, false], [Uint8ClampedArray, false], [Int16Array, false], [Uint16Array, false], [Int32Array, false], [Uint32Array, false], [Float32Array, false], [Float64Array, false]]);
function shouldInstrument(ref) {
var constructor = ref.constructor;
var isBuiltIn = typeof constructor === 'function' && constructor.name in globalObj && globalObj[constructor.name] === constructor;
return !isBuiltIn || handlers.has(constructor);
}
function getHandlers(obj) {
return handlers.get(obj.constructor);
}
var hasOwnProperty$1 = Object.prototype.hasOwnProperty;
var wellKnownSymbols = new Set(Object.getOwnPropertyNames(Symbol).map(function (key) { return Symbol[key]; }).filter(function (value) { return typeof value === 'symbol'; }));
// intercept get operations on observables to know which reaction uses their properties
function get(target, key, receiver) {
var result = Reflect.get(target, key, receiver);
// do not register (observable.prop -> reaction) pairs for well known symbols
// these symbols are frequently retrieved in low level JavaScript under the hood
if (typeof key === 'symbol' && wellKnownSymbols.has(key)) {
return result;
}
// register and save (observable.prop -> runningReaction)
registerRunningReactionForOperation({ target: target, key: key, receiver: receiver, type: 'get' });
// if we are inside a reaction and observable.prop is an object wrap it in an observable too
// this is needed to intercept property access on that object too (dynamic observable tree)
var observableResult = rawToProxy.get(result);
if (hasRunningReaction() && typeof result === 'object' && result !== null) {
if (observableResult) {
return observableResult;
}
// do not violate the none-configurable none-writable prop get handler invariant
// fall back to none reactive mode in this case, instead of letting the Proxy throw a TypeError
var descriptor = Reflect.getOwnPropertyDescriptor(target, key);
if (!descriptor || !(descriptor.writable === false && descriptor.configurable === false)) {
return observable(result);
}
}
// otherwise return the observable wrapper if it is already created and cached or the raw object
return observableResult || result;
}
function has(target, key) {
var result = Reflect.has(target, key);
// register and save (observable.prop -> runningReaction)
registerRunningReactionForOperation({ target: target, key: key, type: 'has' });
return result;
}
function ownKeys(target) {
registerRunningReactionForOperation({ target: target, type: 'iterate' });
return Reflect.ownKeys(target);
}
// intercept set operations on observables to know when to trigger reactions
function set(target, key, value, receiver) {
// make sure to do not pollute the raw object with observables
if (typeof value === 'object' && value !== null) {
value = proxyToRaw.get(value) || value;
}
// save if the object had a descriptor for this key
var hadKey = hasOwnProperty$1.call(target, key);
// save if the value changed because of this set operation
var oldValue = target[key];
// execute the set operation before running any reaction
var result = Reflect.set(target, key, value, receiver);
// do not queue reactions if the target of the operation is not the raw receiver
// (possible because of prototypal inheritance)
if (target !== proxyToRaw.get(receiver)) {
return result;
}
// queue a reaction if it's a new property or its value changed
if (!hadKey) {
queueReactionsForOperation({ target: target, key: key, value: value, receiver: receiver, type: 'add' });
} else if (value !== oldValue) {
queueReactionsForOperation({
target: target,
key: key,
value: value,
oldValue: oldValue,
receiver: receiver,
type: 'set'
});
}
return result;
}
function deleteProperty(target, key) {
// save if the object had the key
var hadKey = hasOwnProperty$1.call(target, key);
var oldValue = target[key];
// execute the delete operation before running any reaction
var result = Reflect.deleteProperty(target, key);
// only queue reactions for delete operations which resulted in an actual change
if (hadKey) {
queueReactionsForOperation({ target: target, key: key, oldValue: oldValue, type: 'delete' });
}
return result;
}
var baseHandlers = { get: get, has: has, ownKeys: ownKeys, set: set, deleteProperty: deleteProperty };
function observable(obj) {
if ( obj === void 0 ) obj = {};
// if it is already an observable or it should not be wrapped, return it
if (proxyToRaw.has(obj) || !shouldInstrument(obj)) {
return obj;
}
// if it already has a cached observable wrapper, return it
// otherwise create a new observable
return rawToProxy.get(obj) || createObservable(obj);
}
function createObservable(obj) {
// if it is a complex built-in object or a normal object, wrap it
var handlers = getHandlers(obj) || baseHandlers;
var observable = new Proxy(obj, handlers);
// save these to switch between the raw object and the wrapped object with ease later
rawToProxy.set(obj, observable);
proxyToRaw.set(observable, obj);
// init basic data structures to save and cleanup later (observable.prop -> reaction) connections
storeObservable(obj);
return observable;
}
function isObservable(obj) {
return proxyToRaw.has(obj);
}
function raw(obj) {
return proxyToRaw.get(obj) || obj;
}
export { observe, unobserve, observable, isObservable, raw };