UNPKG

@nx-js/observer-util

Version:

Simple transparent reactivity with 100% language coverage. Made with ES6 Proxies.

478 lines (426 loc) 17.2 kB
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 };