UNPKG

causalityjs

Version:

A library for reactive programming based on Javascript proxies.

1,618 lines (1,414 loc) 72.6 kB
/*************************************************************** * * State * ***************************************************************/ let state = { inPulse : 0, recordingPaused : 0, observerNotificationNullified : 0, observerNotificationPostponed : 0, }; /*************************************************************** * * Debug & helpers * ***************************************************************/ let trace = { context: false, contextMismatch: false, nestedRepeater: true, }; let objectlog = null; function setObjectlog( newObjectLog ){ objectlog = newObjectLog; // import {objectlog} from './lib/objectlog.js'; // objectlog.configuration.useConsoleDefault = true; } // Debugging function log(entity, pattern) { if( !trace.context ) return; state.recordingPaused++; updateContextState(); objectlog.log(entity, pattern); state.recordingPaused--; updateContextState(); } function logGroup(entity, pattern) { if( !trace.context ) return; state.recordingPaused++; updateContextState(); objectlog.group(entity, pattern); state.recordingPaused--; updateContextState(); } function logUngroup() { if( !trace.context ) return; objectlog.groupEnd(); } function logToString(entity, pattern) { state.recordingPaused++; updateContextState(); let result = objectlog.logToString(entity, pattern); state.recordingPaused--; updateContextState(); return result; } // Helper to quickly get a child array function getArray() { var argumentList = argumentsToArray(arguments); var object = argumentList.shift(); while (argumentList.length > 0) { var key = argumentList.shift(); if (typeof(object[key]) === 'undefined') { if (argumentList.length === 0) { object[key] = []; } else { object[key] = {}; } } object = object[key]; } return object; } function argumentsToArray(argumentList) { return Array.prototype.slice.call(argumentList); } /*************************************************************** * * Array overrides * ***************************************************************/ let staticArrayOverrides = { pop : function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; let index = this.target.length - 1; state.observerNotificationNullified++; let result = this.target.pop(); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'pop']); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, index, [result], null); if (--state.inPulse === 0) postPulseCleanup(); return result; }, push : function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; let index = this.target.length; let argumentsArray = argumentsToArray(arguments); state.observerNotificationNullified++; this.target.push.apply(this.target, argumentsArray); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'push',...argumentsArray]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, index, null, argumentsArray); if (--state.inPulse === 0) postPulseCleanup(); return this.target.length; }, shift : function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; state.observerNotificationNullified++; let result = this.target.shift(); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'shift']); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, 0, [result], null); if (--state.inPulse === 0) postPulseCleanup(); return result; }, unshift : function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; let argumentsArray = argumentsToArray(arguments); state.observerNotificationNullified++; this.target.unshift.apply(this.target, argumentsArray); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'unshift', ...argumentsArray]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, 0, null, argumentsArray); if (--state.inPulse === 0) postPulseCleanup(); return this.target.length; }, splice : function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; let argumentsArray = argumentsToArray(arguments); let index = argumentsArray[0]; let removedCount = argumentsArray[1]; if( typeof argumentsArray[1] === 'undefined' ) removedCount = this.target.length - index; let added = argumentsArray.slice(2); let removed = this.target.slice(index, index + removedCount); state.observerNotificationNullified++; let result = this.target.splice.apply(this.target, argumentsArray); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'splice', ...argumentsArray]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, index, removed, added); if (--state.inPulse === 0) postPulseCleanup(); return result; // equivalent to removed }, copyWithin: function(target, start, end) { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; if( !start ) start = 0; if( !end ) end = this.target.length; if (target < 0) { start = this.target.length - target; } if (start < 0) { start = this.target.length - start; } if (end < 0) { start = this.target.length - end; } end = Math.min(end, this.target.length); start = Math.min(start, this.target.length); if (start >= end) { return; } let removed = this.target.slice(target, target + end - start); let added = this.target.slice(start, end); state.observerNotificationNullified++; let result = this.target.copyWithin(target, start, end); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'copyWithin', start, end]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, target, added, removed); if (--state.inPulse === 0) postPulseCleanup(); return result; } }; ['reverse', 'sort', 'fill'].forEach(function(functionName) { staticArrayOverrides[functionName] = function() { if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; let argumentsArray = argumentsToArray(arguments); let removed = this.target.slice(0); state.observerNotificationNullified++; let result = this.target[functionName] .apply(this.target, argumentsArray); state.observerNotificationNullified--; if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,functionName,...argumentsArray]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } emitSpliceEvent(this, 0, removed, this.target.slice(0)); if (--state.inPulse === 0) postPulseCleanup(); return result; }; }); let nextId = 1; function resetObjectIds() { nextId = 1; } /*************************************************************** * * Array Handlers * ***************************************************************/ function getHandlerArray(target, key) { if (this.overrides.__overlay !== null && (typeof(overlayBypass[key]) === 'undefined')) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.get.apply(overlayHandler, [overlayHandler.target, key]); } if (staticArrayOverrides[key]) { return staticArrayOverrides[key].bind(this); } else if (typeof(this.overrides[key]) !== 'undefined') { return this.overrides[key]; } else { if (inActiveRecording) { if (this._arrayObservers === null) { this._arrayObservers = { handler: this }; } registerAnyChangeObserver(key, this._arrayObservers);//object } return target[key]; } } function setHandlerArray(target, key, value) { if (this.overrides.__overlay !== null) { if (key === "__overlay") { this.overrides.__overlay = value; return true; } else { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.set.apply( overlayHandler, [overlayHandler.target, key, value]); } } let previousValue = target[key]; // If same value as already set, do nothing. if (key in target) { if (previousValue === value || (Number.isNaN(previousValue) && Number.isNaN(value)) ) { return true; } } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; if (!isNaN(key)) { // Number index if (typeof(key) === 'string') { key = parseInt(key); } target[key] = value; if( target[key] === value || ( Number.isNaN(target[key]) && Number.isNaN(value)) ) { // Write protected? emitSpliceReplaceEvent(this, key, value, previousValue); if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'set', key,value]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } } } else { // String index target[key] = value; if( target[key] === value || (Number.isNaN(target[key]) && Number.isNaN(value)) ) { // Write protected? emitSetEvent(this, key, value, previousValue); if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'set',key,value]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } } } if (--state.inPulse === 0) postPulseCleanup(); if( target[key] !== value && !(Number.isNaN(target[key]) && Number.isNaN(value)) ) return false; // Write protected? return true; } function deletePropertyHandlerArray(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.deleteProperty.apply( overlayHandler, [overlayHandler.target, key]); } if (!(key in target)) { return true; } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return true; state.inPulse++; let previousValue = target[key]; delete target[key]; if(!( key in target )) { // Write protected? emitDeleteEvent(this, key, previousValue); if (this._arrayObservers !== null) { if(recordChanges) changes.push([this.overrides.__id,'delete', key]); notifyChangeObservers("_arrayObservers", this._arrayObservers); } } if (--state.inPulse === 0) postPulseCleanup(); if( key in target ) return false; // Write protected? return true; } function ownKeysHandlerArray(target) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.ownKeys.apply( overlayHandler, [overlayHandler.target]); } if (inActiveRecording) { if (this._arrayObservers === null) { this._arrayObservers = { handler: this }; } registerAnyChangeObserver("[]", this._arrayObservers); } let result = Object.keys(target); result.push('length'); return result; } function hasHandlerArray(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.has.apply(overlayHandler, [target, key]); } if (inActiveRecording) { if (this._arrayObservers === null) { this._arrayObservers = { handler: this }; } registerAnyChangeObserver("[]", this._arrayObservers); } return key in target; } function definePropertyHandlerArray(target, key, oDesc) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.defineProperty.apply( overlayHandler, [overlayHandler.target, key, oDesc]); } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; if (this._arrayObservers !== null) { notifyChangeObservers("_arrayObservers", this._arrayObservers); } if (--state.inPulse === 0) postPulseCleanup(); return target; } function getOwnPropertyDescriptorHandlerArray(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.getOwnPropertyDescriptor.apply( overlayHandler, [overlayHandler.target, key]); } if (inActiveRecording) { if (this._arrayObservers === null) { this._arrayObservers = { handler: this }; } registerAnyChangeObserver("[]", this._arrayObservers); } return Object.getOwnPropertyDescriptor(target, key); } /*************************************************************** * * Object Handlers * ***************************************************************/ function getHandlerObject(target, key) { //key = key.toString(); if (this.overrides.__overlay !== null && key !== "__overlay" && (typeof(overlayBypass[key]) === 'undefined')) { let overlayHandler = this.overrides.__overlay.__handler; let result = overlayHandler.get.apply( overlayHandler, [overlayHandler.target, key]); return result; } if (typeof(this.overrides[key]) !== 'undefined') { return this.overrides[key]; } else { if (typeof(key) !== 'undefined') { if (inActiveRecording) { const keyStr = key.toString(); // convert symbols to strings if (typeof(this._propertyObservers) === 'undefined') { this._propertyObservers = { handler: this }; } if (typeof(this._propertyObservers[keyStr]) === 'undefined') { this._propertyObservers[keyStr] = { handler: this }; } registerAnyChangeObserver(keyStr, this._propertyObservers[keyStr]); } let scan = target; while ( scan !== null && typeof(scan) !== 'undefined' ) { let descriptor = Object.getOwnPropertyDescriptor(scan, key); if (typeof(descriptor) !== 'undefined' && typeof(descriptor.get) !== 'undefined') { return descriptor.get.bind(this.overrides.__proxy)(); } scan = Object.getPrototypeOf( scan ); } return target[key]; } } } function setHandlerObject(target, key, value) { if (this.overrides.__overlay !== null) { if (key === "__overlay") { this.overrides.__overlay = value; // Setting a new overlay, should not be possible? return true; } else { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.set.apply( overlayHandler, [overlayHandler.target, key, value]); } } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; let previousValue = target[key]; // If same value as already set, do nothing. if (key in target) { if (previousValue === value || (Number.isNaN(previousValue) && Number.isNaN(value)) ) { return true; } } state.inPulse++; let undefinedKey = !(key in target); target[key] = value; let resultValue = target[key]; //console.log("result", undefinedKey, resultValue, (resultValue === value)); if( resultValue === value || (Number.isNaN(resultValue) && Number.isNaN(value)) ) { // Write protected? if (typeof(this._propertyObservers) !== 'undefined' && typeof(this._propertyObservers[key]) !== 'undefined') { if(recordChanges) changes.push([this.overrides.__id,'set', key,value]); notifyChangeObservers("_propertyObservers." + key, this._propertyObservers[key]); } if (undefinedKey) { if (typeof(this._enumerateObservers) !== 'undefined') { if(recordChanges) changes.push([this.overrides.__id,'set', key,value]); notifyChangeObservers("_enumerateObservers", this._enumerateObservers); } } emitSetEvent(this, key, value, previousValue); } if (--state.inPulse === 0) postPulseCleanup(); if( resultValue !== value && !(Number.isNaN(resultValue) && Number.isNaN(value))) return false; // Write protected? return true; } function deletePropertyHandlerObject(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; overlayHandler.deleteProperty.apply( overlayHandler, [overlayHandler.target, key]); return true; } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return true; if (!(key in target)) { return true; } else { state.inPulse++; let previousValue = target[key]; delete target[key]; if(!( key in target )) { // Write protected? emitDeleteEvent(this, key, previousValue); if (typeof(this._enumerateObservers) !== 'undefined') { if(recordChanges) changes.push([this.overrides.__id,'delete', key]); notifyChangeObservers("_enumerateObservers", this._enumerateObservers); } } if (--state.inPulse === 0) postPulseCleanup(); if( key in target ) return false; // Write protected? return true; } } function ownKeysHandlerObject(target, key) { // Not inherited? if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.ownKeys.apply( overlayHandler, [overlayHandler.target, key]); } if (inActiveRecording) { if (typeof(this._enumerateObservers) === 'undefined') { this._enumerateObservers = { handler: this }; } registerAnyChangeObserver("[]", this._enumerateObservers); } let keys = Object.keys(target); // keys.push('__id'); return keys; } function hasHandlerObject(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.has.apply( overlayHandler, [overlayHandler.target, key]); } if (inActiveRecording) { if (typeof(this._enumerateObservers) === 'undefined') { this._enumerateObservers = { handler: this }; } registerAnyChangeObserver("[]", this._enumerateObservers); } return key in target; } function definePropertyHandlerObject(target, key, descriptor) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.defineProperty.apply( overlayHandler, [overlayHandler.target, key]); } if (writeRestriction !== null && typeof(writeRestriction[this.overrides.__id]) === 'undefined') return; state.inPulse++; if (typeof(this._enumerateObservers) !== 'undefined') { notifyChangeObservers("_enumerateObservers", this._enumerateObservers); } if (--state.inPulse === 0) postPulseCleanup(); return Reflect.defineProperty(target, key, descriptor); } function getOwnPropertyDescriptorHandlerObject(target, key) { if (this.overrides.__overlay !== null) { let overlayHandler = this.overrides.__overlay.__handler; return overlayHandler.getOwnPropertyDescriptor .apply(overlayHandler, [overlayHandler.target, key]); } if (inActiveRecording) { if (typeof(this._enumerateObservers) === 'undefined') { this._enumerateObservers = { handler: this }; } registerAnyChangeObserver("[]", this._enumerateObservers); } return Object.getOwnPropertyDescriptor(target, key); } /*************************************************************** * * Create * ***************************************************************/ function create(createdTarget, cacheId) { state.inPulse++; if (typeof(createdTarget) === 'undefined') { createdTarget = {}; } if (typeof(cacheId) === 'undefined') { cacheId = null; } let handler; if (createdTarget instanceof Array) { handler = { _arrayObservers : null, // getPrototypeOf: function () {}, // setPrototypeOf: function () {}, // isExtensible: function () {}, // preventExtensions: function () {}, // apply: function () {}, // construct: function () {}, get: getHandlerArray, set: setHandlerArray, deleteProperty: deletePropertyHandlerArray, ownKeys: ownKeysHandlerArray, has: hasHandlerArray, defineProperty: definePropertyHandlerArray, getOwnPropertyDescriptor: getOwnPropertyDescriptorHandlerArray }; } else { // let _propertyObservers = { handler: this }; // for (property in createdTarget) { // _propertyObservers[property] = {}; // } handler = { // getPrototypeOf: function () {}, // setPrototypeOf: function () {}, // isExtensible: function () {}, // preventExtensions: function () {}, // apply: function () {}, // construct: function () {}, // _enumerateObservers : {}, // _propertyObservers: _propertyObservers, get: getHandlerObject, set: setHandlerObject, deleteProperty: deletePropertyHandlerObject, ownKeys: ownKeysHandlerObject, has: hasHandlerObject, defineProperty: definePropertyHandlerObject, getOwnPropertyDescriptor: getOwnPropertyDescriptorHandlerObject }; } handler.target = createdTarget; let proxy = new Proxy(createdTarget, handler); handler.overrides = { __id: nextId++, __cacheId : cacheId, __overlay : null, __target: createdTarget, __handler : handler, __proxy : proxy, constructor : createdTarget.constructor, // This inside these functions will be the Proxy. Change to handler? repeat : genericRepeatFunction, tryStopRepeat : genericStopRepeatFunction, observe: genericObserveFunction, cached : genericCallAndCacheFunction, cachedInCache : genericCallAndCacheInCacheFunction, reCached : genericReCacheFunction, reCachedInCache : genericReCacheInCacheFunction, tryUncache : genericUnCacheFunction, // reCache aliases project : genericReCacheFunction, projectInProjectionOrCache : genericReCacheInCacheFunction, // Identity and state mergeFrom : genericMergeFrom, forwardTo : genericForwarder, removeForwarding : genericRemoveForwarding, mergeAndRemoveForwarding: genericMergeAndRemoveForwarding }; if (inReCache !== null) { if (cacheId !== null && typeof(inReCache.cacheIdObjectMap[cacheId]) !== 'undefined') { // Overlay previously created let infusionTarget = inReCache.cacheIdObjectMap[cacheId]; infusionTarget.__handler.overrides.__overlay = proxy; inReCache.newlyCreated.push(infusionTarget); return infusionTarget; // Borrow identity of infusion target. } else { // Newly created in this reCache cycle. Including overlaid ones. inReCache.newlyCreated.push(proxy); } } if (writeRestriction !== null) { writeRestriction[proxy.__id] = true; } emitCreationEvent(handler); if (--state.inPulse === 0) postPulseCleanup(); return proxy; } /********************************** * * Causality Global stack * **********************************/ let independentContext = null; let context = null; let inActiveRecording = false; let activeRecorder = null; let inCachedCall = null; let inReCache = null; function updateContextState() { inActiveRecording = (context !== null) ? ((context.type === "recording") && state.recordingPaused === 0) : false; activeRecorder = (inActiveRecording) ? context : null; inCachedCall = null; inReCache = null; if (independentContext !== null) { inCachedCall = (independentContext.type === "cached_call") ? independentContext : null; inReCache = (independentContext.type === "reCache") ? independentContext : null; } } function removeChildContexts(context) { //console.warn("removeChildContexts " + context.type, context.id||'');//DEBUG trace.context && logGroup(`removeChildContexts: ${context.type} ${context.id}`); if (context.child !== null && !context.child.independent) { context.child.removeContextsRecursivley(); } context.child = null; // always remove if (context.children !== null) { context.children.forEach(function (child) { if (!child.independent) { child.removeContextsRecursivley(); } }); } context.children = []; // always clear trace.context && logUngroup(); } function removeContextsRecursivley() { trace.context && logGroup(`removeContextsRecursivley ${this.id}`); this.remove(); this.isRemoved = true; removeChildContexts(this); trace.context && logUngroup(); } // Optimization, do not create array if not needed. function addChild(context, child) { if (context.child === null && context.children === null) { context.child = child; } else if (context.children === null) { context.children = [context.child, child]; context.child = null; } else { context.children.push(child); } } // occuring types: recording, repeater_refreshing, // cached_call, reCache, block_side_effects function enterContext(type, enteredContext) { logGroup(`enterContext: ${type} ${enteredContext.id} ${enteredContext.description}`); if (typeof(enteredContext.initialized) === 'undefined') { // Initialize context enteredContext.removeContextsRecursivley = removeContextsRecursivley; enteredContext.parent = null; enteredContext.independentParent = independentContext; enteredContext.type = type; enteredContext.child = null; enteredContext.children = null; enteredContext.directlyInvokedByApplication = (context === null); // Connect with parent if (context !== null) { addChild(context, enteredContext) enteredContext.parent = context; // Even a shared context like a cached call only has // the first callee as its parent. Others will just observe it. } else { enteredContext.independent = true; } if (enteredContext.independent) { independentContext = enteredContext; } enteredContext.initialized = true; } else { if (enteredContext.independent) { independentContext = enteredContext; } else { independentContext = enteredContext.independentParent; } } // if (!enteredContext.independent) // if (!enteredContext.independent) if (independentContext === null && !enteredContext.independent) { throw new Error("should be!!"); } context = enteredContext; updateContextState(); logUngroup(); return enteredContext; } function leaveContext( activeContext ) { // DEBUG if( context && activeContext && (context !== activeContext) && !context.independent ){ console.trace("leaveContext mismatch " + activeContext.type, activeContext.id||''); if( !context ) console.log('current context null'); else { console.log("current context " + context.type, context.id||''); if( context.parent ){ console.log("parent context " + context.parent.type, context.parent.id||''); } } } if( context && activeContext === context ) { //console.log("leaveContext " + activeContext.type, activeContext.id||'', "to", context.parent ? context.parent.id : 'null');//DEBUG if (context.independent) { independentContext = context.independentParent; } context = context.parent; } updateContextState(); } function enterIndependentContext() { return enterContext("independently", { independent : true, remove : () => {} }); } function leaveIndependentContext( activeContext ) { leaveContext( activeContext ); } function independently(action) { const activeContext = enterContext("independently", { independent : true, remove : () => {} }); action(); leaveContext( activeContext ); } /* exists as a fallback for async recording without active * contexts. Used when not in recording context. */ function emptyContext(){ return { record( action ){ return action() } } } /********************************** * Pulse & Transactions * * Upon change do **********************************/ // A sequence of transactions, end with cleanup. function pulse(callback) { state.inPulse++; callback(); if (--state.inPulse === 0) postPulseCleanup(); } // Single transaction, end with cleanup. const transaction = postponeObserverNotification; function postponeObserverNotification(callback) { state.inPulse++; state.observerNotificationPostponed++; callback(); state.observerNotificationPostponed--; proceedWithPostponedNotifications(); if (--state.inPulse === 0) postPulseCleanup(); } let contextsScheduledForPossibleDestruction = []; function postPulseCleanup() { // trace.context && logGroup( // "postPulseCleanup: " + // contextsScheduledForPossibleDestruction.length); // log("post pulse cleanup"); contextsScheduledForPossibleDestruction.forEach(function(context) { // log(context.directlyInvokedByApplication); trace.context && logGroup( "... consider remove context: " + context.type); if (!context.directlyInvokedByApplication) { trace.context && log( "... not directly invoked by application... "); if (emptyObserverSet(context.contextObservers)) { trace.context && log("... empty observer set... "); trace.context && log( "Remove a context since it has no more observers, " + "and is not directly invoked by application: " + context.type); context.removeContextsRecursivley(); } else { trace.context && log("... not empty observer set"); } } trace.context && logUngroup(); }); contextsScheduledForPossibleDestruction = []; postPulseHooks.forEach(function(callback) { callback(events); }); trace.context && logUngroup(); if (recordEvents) events = []; if (recordChanges) changes.length = 0; } let postPulseHooks = []; function addPostPulseAction(callback) { postPulseHooks.push(callback); } function removeAllPostPulseActions() { postPulseHooks = []; } /********************************** * * Observe * **********************************/ let recordEvents = false; function setRecordEvents(value) { recordEvents = value; } let recordChanges = false; let changes = []; function setRecordChanges(value) { recordChanges = value; } function emitSpliceEvent(handler, index, removed, added) { if (recordEvents || typeof(handler.observers) !== 'undefined') { emitEvent(handler, {type: 'splice', index, removed, added}); } } function emitSpliceReplaceEvent(handler, key, value, previousValue) { if (recordEvents || typeof(handler.observers) !== 'undefined') { emitEvent(handler, { type: 'splice', index: key, removed: [previousValue], added: [value] }); } } function emitSetEvent(handler, key, value, previousValue) { if (recordEvents || typeof(handler.observers) !== 'undefined') { emitEvent(handler, { type: 'set', property: key, newValue: value, oldValue: previousValue}); } } function emitDeleteEvent(handler, key, previousValue) { if (recordEvents || typeof(handler.observers) !== 'undefined') { emitEvent(handler, { type: 'delete', property: key, deletedValue: previousValue}); } } function emitCreationEvent(handler) { if (recordEvents) { emitEvent(handler, {type: 'create'}) } } let events = []; function emitEvent(handler, event) { event.object = handler.overrides.__proxy; if (recordEvents) { events.push(event); } // log(event); event.objectId = handler.overrides.__id; if (typeof(handler.observers) !== 'undefined') { for (let id in handler.observers) { handler.observers[id](event); } } } function observeAll(array, callback) { array.forEach(function(element) { element.observe(callback); }); } let nextObserverId = 0; function genericObserveFunction(observerFunction) { //, independent let handler = this.__handler; let observer = { // independent : independent, //! wrap with independent context instead! id : nextObserverId++, handler : handler, remove : function() { trace.context && log("remove observe..."); delete this.handler.observers[this.id]; } // observerFunction : observerFunction, // not needed... } const activeContext = enterContext("observe", observer); if (typeof(handler.observers) === 'undefined') { handler.observers = {}; } handler.observers[observer.id] = observerFunction; leaveContext( activeContext ); return observer; } /********************************** * Dependency recording * * Upon change do **********************************/ let recorderId = 0; function uponChangeDo() { // description(optional), doFirst, doAfterChange. doAfterChange // cannot modify model, if needed, use a repeater instead. // (for guaranteed consistency) // Arguments let doFirst; let doAfterChange; let description = null; if (arguments.length > 2) { description = arguments[0]; doFirst = arguments[1]; doAfterChange = arguments[2]; } else { doFirst = arguments[0]; doAfterChange = arguments[1]; } // Recorder context const enteredContext = enterContext('recording', { independent : false, nextToNotify: null, id: recorderId++, description: description, sources : [], uponChangeAction: doAfterChange, sourcesString() { let result = ""; for (let source of this.sources) { while (source.parent) source = source.parent; const handler = source.handler; if( !handler ){ console.log("source without handler", source); continue; } const isDefToStr = [ Object.prototype.toString, Array.prototype.toString ].includes(handler.target.toString); const sourceStr = isDefToStr ? handler.overrides.__id : handler.target.toString.call(handler.overrides.__proxy); const keyStr = source.key.toString ? source.key.toString() : source.key; result += sourceStr + "." + keyStr + "\n"; } return result; }, remove : function() { trace.context && logGroup(`remove recording ${this.id}`); // Clear out previous observations this.sources.forEach(function(observerSet) { // From observed object // let observerSetContents = getMap( // observerSet, 'contents'); // if (typeof(observerSet['contents'])) { ////! Should not be needed // observerSet['contents'] = {}; // } let observerSetContents = observerSet['contents']; delete observerSetContents[this.id]; let noMoreObservers = false; observerSet.contentsCounter--; // trace.context && log( // "observerSet.contentsCounter: " + // observerSet.contentsCounter); if (observerSet.contentsCounter == 0) { if (observerSet.isRoot) { if (observerSet.first === null && observerSet.last === null) { noMoreObservers = true; } } else { if (observerSet.parent.first === observerSet) { observerSet.parent.first === observerSet.next; } if (observerSet.parent.last === observerSet) { observerSet.parent.last === observerSet.previous; } if (observerSet.next !== null) { observerSet.next.previous = observerSet.previous; } if (observerSet.previous !== null) { observerSet.previous.next = observerSet.next; } observerSet.previous = null; observerSet.next = null; if (observerSet.parent.first === null && observerSet.parent.last === null) { noMoreObservers = true; } } if (noMoreObservers && typeof(observerSet.noMoreObserversCallback) !== 'undefined') { observerSet.noMoreObserversCallback(); } } }.bind(this)); this.sources.length = 0; // From repeater itself. trace.context && logUngroup(); } }); // Method for continue async in same context enteredContext.record = function( action ){ if( context == enteredContext || enteredContext.isRemoved ) return action(); //console.log('enteredContext.record');//DEBUG const activeContext = enterContext(enteredContext.type, enteredContext); const value = action(); leaveContext( activeContext ); return value; } //console.log("doFirst in context " + enteredContext.type, enteredContext.id||'');//DEBUG let returnValue = doFirst( enteredContext ); //if( context ) console.log("after doFirst context " + enteredContext.type, enteredContext.id||'');//DEBUG leaveContext( enteredContext ); return returnValue; } function withoutRecording(action) { state.recordingPaused++; updateContextState(); action(); state.recordingPaused--; updateContextState(); } function emptyObserverSet(observerSet) { return observerSet.contentsCounter === 0 && observerSet.first === null; } let sourcesObserverSetChunkSize = 500; function registerAnyChangeObserver(key, observerSet) { // instance can be a cached method if observing its return value, // object & definition only needed for debugging. if (activeRecorder !== null) { if (typeof(observerSet.initialized) === 'undefined') { observerSet.key = key; observerSet.isRoot = true; observerSet.contents = {}; observerSet.contentsCounter = 0; observerSet.initialized = true; observerSet.first = null; observerSet.last = null; } let recorderId = activeRecorder.id; if (typeof(observerSet.contents[recorderId]) !== 'undefined') { return; } if (observerSet.contentsCounter === sourcesObserverSetChunkSize && observerSet.last !== null) { observerSet = observerSet.last; if (typeof(observerSet.contents[recorderId]) !== 'undefined') { return; } } if (observerSet.contentsCounter === sourcesObserverSetChunkSize) { let newChunk = { isRoot : false, contents: {}, contentsCounter: 0, next: null, previous: null, parent: null }; if (observerSet.isRoot) { newChunk.parent = observerSet; observerSet.first = newChunk; observerSet.last = newChunk; } else { observerSet.next = newChunk; newChunk.previous = observerSet; newChunk.parent = observerSet.parent; observerSet.parent.last = newChunk; } observerSet = newChunk; } // Add repeater on object beeing observed, // if not already added before let observerSetContents = observerSet.contents; if (typeof(observerSetContents[recorderId]) === 'undefined') { observerSet.contentsCounter = observerSet.contentsCounter + 1; observerSetContents[recorderId] = activeRecorder; // Note dependency in repeater itself (for cleaning up) activeRecorder.sources.push(observerSet); } } } /** ------------- * Upon change * -------------- */ let nextObserverToNotifyChange = null; let lastObserverToNotifyChange = null; function proceedWithPostponedNotifications() { //console.log("proceedWithPostponedNotifications", state.observerNotificationPostponed, "next is", nextObserverToNotifyChange?.id || "none", "last is", lastObserverToNotifyChange?.id || "none"); if (state.observerNotificationPostponed == 0) { //console.log("proceedWithPostponedNotifications START"); while (nextObserverToNotifyChange !== null) { let recorder = nextObserverToNotifyChange; nextObserverToNotifyChange = recorder.nextToNotify; //const repeater = recorder.parent; //console.log(recorder.type, recorder.id, repeater.type, repeater.id + "/" + repeater.description, "uponChangeAction"); recorder.uponChangeAction(); } lastObserverToNotifyChange = null; //console.log("proceedWithPostponedNotifications END\n"); } } function nullifyObserverNotification(callback) { state.observerNotificationNullified++; callback(); state.observerNotificationNullified--; } // Recorders is a map from id => recorder function notifyChangeObservers(description, observers) { //console.log("notifyChangeObservers", description, observers.handler.overrides.__id, "in", context?.id||"top", ":", Object.values(observers.contents).map( context => context.parent.id + "/" + context.parent.description + "\n" + context.sourcesString() ).join("\n") ); if (typeof(observers.initialized) !== 'undefined') { if (state.observerNotificationNullified > 0) { return; } let contents = observers.contents; for (let id in contents) { notifyChangeObserver(contents[id]); } if (typeof(observers.first) !== 'undefined') { let chainedObserverChunk = observers.first; while(chainedObserverChunk !== null) { let contents = chainedObserverChunk.contents; for (let id in contents) { notifyChangeObserver(contents[id]); } chainedObserverChunk = chainedObserverChunk.next; } } } } function notifyChangeObserver(observer) { if (observer != context) { if( trace.contextMismatch && context && context.id ){ console.log("notifyChangeObserver mismatch " + observer.type, observer.id||''); if( !context ) console.log('current context null'); else { console.log("current context " + context.type, context.id||''); if( context.parent ){ console.log("parent context " + context.parent.type, context.parent.id||''); } } } //console.log("recorder", observer.id, "parent", observer.parent.id + "/" + observer.parent.description, "remove"); observer.remove(); // Cannot be any more dirty than it already is! if (state.observerNotificationPostponed > 0) { //console.log("recorder", observer.id, "NotificationPostponed", state.observerNotificationPostponed, "next is", nextObserverToNotifyChange?.id || "none", "last is", lastObserverToNotifyChange?.id || "none"); if (lastObserverToNotifyChange !== null) { //console.log("Adding next", observer.id ,"to last", lastObserverToNotifyChange.id ); lastObserverToNotifyChange.nextToNotify = observer; if (nextObserverToNotifyChange === null) nextObserverToNotifyChange = observer; } else { nextObserverToNotifyChange = observer; } lastObserverToNotifyChange = observer; } else { //console.log("recorder", observer.id, "uponChangeAction"); // blockSideEffects(function() { observer.uponChangeAction(); // }); } } } /********************************** * * Repetition * **********************************/ let firstDirtyRepeater = null; let lastDirtyRepeater = null; function clearRepeaterLists() { recorderId = 0; firstDirtyRepeater = null; lastDirtyRepeater = null; } function detatchRepeater(repeater) { if (lastDirtyRepeater === repeater) { lastDirtyRepeater = repeater.previousDirty; } if (firstDirtyRepeater === repeater) { firstDirtyRepeater = repeater.nextDirty; } if (repeater.nextDirty) { repeater.nextDirty.previousDirty = repeater.previousDirty; } if (repeater.previousDirty) { repeater.previousDirty.nextDirty = repeater.nextDirty; } //# TODO: validate with test repeater.previousDirty = null; repeater.nextDirty = null; } let repeaterId = 0; function repeatOnChange() { // description(optional), action // Arguments let description = ''; let repeaterAction; let repeaterNonRecordingAction = null; let options = {}; const args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments)); if (typeof(args[0]) === 'string') { description = args.shift(); } if (typeof(args[0]) === 'function') { repeaterAction = args.shift(); } if (typeof(args[0]) === 'function') { repeaterNonRecordingAction = args.shift(); } if (typeof(args[0]) === 'object') { options = args.shift(); } if( trace.nestedRepeater && inActiveRecording ){ let parentDesc = activeRecorder.description; if( !parentDesc && activeRecorder.parent ) parentDesc = activeRecorder.parent.description; if( !parentDesc ) parentDesc = 'unnamed'; console.warn(`repeater ${description||'unnamed'} inside active recording ${parentDesc}`); } // Activate! return refreshRepeater({ independent : false, id: repeaterId++, description: description, repeaterAction : repeaterAction, nonRecordedAction: repeaterNonRecordingAction, options: options, remove: function() { log("remove repeater_refreshing"); //" + this.id + "." + this.description); detatchRepeater(this); // removeSingleChildContext(this); // Remove recorder! // removeChildContexts(this); }, causalityString() { const context = this.invalidatedInContext; const object = this.invalidatedByObject; if (!object) return "Repeater started: " + this.description const key = this.invalidatedByKey; // let objectClassName; // withoutRecording(() => { // objectClassName = object.constructor.name; // }); const contextString = (context ? context.description : "outside repeater/invalidator") // const causeString = objectClassName + ":" + (object.causality.buildId ? object.causality.buildId : object.causality.id) + "." + key + " (modified)"; const causeString = " " + object.toString() + "." + key + ""; const effectString = "" + this.description + ""; return "(" + contextString + ")" + causeString + " --> " + effectString; }, creationString() { let result = "{"; result += "created: " + this.createdCount + ", "; result += "createdTemporary:" + this.createdTemporaryCount + ", "; result += "removed:" + this.removedCount + "}"; return result; }, sourcesString() { let result = ""; for (let source of this.sources) { while (source.parent) source = source.parent; result += source.handler.proxy.toString() + "." + source.key + "\n"; } return result; }, nextDirty : null, previousDirty : null, lastRepeatTime: 0, lastCallTime: 0, lastInvokeTime: 0, }); } function refreshRepeater(repeater) { let time = Date.now(); if( !repeater.options ) repeater.options = {}; const options = repeater.options; const timeSinceLastRepeat = time - repeater.lastRepeatTime; if( options.throttle && options.throttle > timeSinceLastRepeat ){ const waiting = options.throttle - timeSinceLastRepeat; //console.log(`Delayed repeater for ${waiting}`); setTimeout(()=>refreshRepeater(repeater), waiting); return repeater; // come back later } const activeContext = enterContext('repeater_refreshing', repeater); repeater.returnValue = uponChangeDo( enteredContext => repeater.repeaterAction(enteredContext,repeater), function () { // unlockSideEffects(function() { if( context && !context.independent ){ //console.log("deferring repeaterDirty", context.id); // defer repeater if we are in a nested context //setTimeout(()=>{ independently(()=>{ //console.log("deferred repeaterDirty", context.id); repeaterDirty(repeater); }); } else { repeaterDirty(repeater); } // }); } ); if (repeater.lastTimerId){ clearTimeout(repeater.lastTimerId); repeater.lastTimerId = null; //console.log(`Delayed NRA cancelled`); } if (repeater.nonRecordedAction !== null) { let waiting = 0; if( options.throttle || options.nonRecordedDebounce ){ // const timeSinceLastRepeat = time - repeater.lastRepeatTime; const timeSinceLastCall = time - repeater.lastCallTime; const timeSinceLastInvoke = time - repeater.lastInvokeTime; const waitInvoke = options.nonRecordedDebounce || options.throttle * 2; //console.log(`lastRepeat ${timeSinceLastRepeat}, //lastcall ${timeSinceLast