From 6cfa5693667a327afaa82eafbe54f53f1af5a5d7 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 1 Jun 2026 01:21:09 -0700 Subject: [PATCH] wip - still debugging --- core/event/event-manager.wip.js | 4003 +++++++++++++ core/event/mutable-event.wip.js | 734 +++ data/model/data-identifier.js | 1 + data/model/data-identifier.mjson | 6 +- data/model/data-object.js | 104 +- data/model/data-object.mjson | 14 + data/model/transaction.js | 967 +++- data/service/data-service.js | 168 +- data/service/data-service.wip.js | 9309 ++++++++++++++++++++++++++++++ worker/data-worker.js | 2 +- 10 files changed, 14980 insertions(+), 328 deletions(-) create mode 100644 core/event/event-manager.wip.js create mode 100644 core/event/mutable-event.wip.js create mode 100644 data/service/data-service.wip.js diff --git a/core/event/event-manager.wip.js b/core/event/event-manager.wip.js new file mode 100644 index 000000000..ba35689f8 --- /dev/null +++ b/core/event/event-manager.wip.js @@ -0,0 +1,4003 @@ +/*global Window, Document, Element, Event, Components, Touch, MontageElement */ + +/** + * @author Lea Verou + * @license MIT + * @see http://leaverou.github.com/chainvas/ + */ + +/** + * @module mod/core/event/event-manager + * @requires mod/core/core + * @requires mod/core/event/mutable-event + * @requires mod/core/serialization + * @requires mod/core/event/action-event-listener + */ + +var Montage = require("../core").Montage, + MutableEvent = require("./mutable-event").MutableEvent, + Serializer = require("../serialization/serializer/montage-serializer").MontageSerializer, + Deserializer = require("../serialization/deserializer/montage-deserializer").MontageDeserializer, + Map = require("../collections/map"), + currentEnvironment = require("../environment").currentEnvironment, + ChangeEvent = require("./change-event").ChangeEvent, + Event_NONE = 0, + Event_CAPTURING_PHASE = 1, + Event_AT_TARGET = 2, + Event_BUBBLING_PHASE = 3, + defaultEventManager, + browserSupportsCaptureOption = false, + browserSupportsPassiveOption = false, + MontageElement = (typeof window === "object") ? window.MontageElement : null; + + + + +/** + * Notes to integrate coomposers in event manager: + * + * 1. Composers have to be registered. + * - Composer's code should register themeselves + * - Component relying on them today already require them + * - later the tool can have a map of event names -> moduleId and + * automatically add to the root serialization, or each template, the modules needed. + * + * 2. addEventListner("eventName") + * - if not standard, find in map the composer registered if any + * - instantiate one + * - if listener is a component, set the relationship as before + * - load, etc... which make the composer add it's own listeners + * - add the composer to the options object. + */ + + + + + +/** + * add|removeEventListener options related feature detection for older browsers + */ +try { + var options = { + get passive() { + browserSupportsPassiveOption = true; + return false; + }, + get capture() { + browserSupportsCaptureOption = true; + return false; + } + }; + addEventListener("test", null, options); + removeEventListener("test", null, options); +} catch(e) {} + + +/** + * If needed, polyfill for Event Listener with Options + * from: + * https://raw.githubusercontent.com/WICG/EventListenerOptions/gh-pages/EventListenerOptions.polyfill.js + * + */ +if (typeof EventTarget === "function" && (!browserSupportsPassiveOption || !browserSupportsCaptureOption)) { + (function() { + var super_add_event_listener = EventTarget.prototype.addEventListener; + var super_remove_event_listener = EventTarget.prototype.removeEventListener; + var super_prevent_default = Event.prototype.preventDefault; + + function parseOptions(type, listener, options, action) { + var needsWrapping = false; + var useCapture = false; + var passive = false; + var fieldId; + if (options) { + if (typeof(options) === 'object') { + passive = options.passive ? true : false; + useCapture = options.useCapture ? true : false; + } else { + useCapture = options; + } + } + if (passive) + needsWrapping = true; + if (needsWrapping) { + fieldId = useCapture.toString(); + fieldId += passive.toString(); + } + action(needsWrapping, fieldId, useCapture, passive); + } + + Event.prototype.preventDefault = function() { + if (this.__passive) { + console.warn("Ignored attempt to preventDefault an event from a passive listener"); + return; + } + super_prevent_default.apply(this); + } + + EventTarget.prototype.addEventListener = function(type, listener, options) { + var super_this = this; + parseOptions(type, listener, options, + function(needsWrapping, fieldId, useCapture, passive) { + if (needsWrapping) { + var fieldId = useCapture.toString(); + fieldId += passive.toString(); + + if (!this.__event_listeners_options) + this.__event_listeners_options = {}; + if (!this.__event_listeners_options[type]) + this.__event_listeners_options[type] = {}; + if (!this.__event_listeners_options[type][listener]) + this.__event_listeners_options[type][listener] = []; + if (this.__event_listeners_options[type][listener][fieldId]) + return; + var wrapped = { + handleEvent: function (e) { + e.__passive = passive; + if (typeof(listener) === 'function') { + listener(e); + } else { + listener.handleEvent(e); + } + e.__passive = false; + } + }; + this.__event_listeners_options[type][listener][fieldId] = wrapped; + super_add_event_listener.call(super_this, type, wrapped, useCapture); + } else { + super_add_event_listener.call(super_this, type, listener, useCapture); + } + }); + } + + EventTarget.prototype.removeEventListener = function(type, listener, options) { + var super_this = this; + parseOptions(type, listener, options, + function(needsWrapping, fieldId, useCapture, passive) { + if (needsWrapping && + this.__event_listeners_options && + this.__event_listeners_options[type] && + this.__event_listeners_options[type][listener] && + this.__event_listeners_options[type][listener][fieldId]) { + super_remove_event_listener.call(super_this, type, this.__event_listeners_options[type][listener][fieldId], false); + delete this.__event_listeners_options[type][listener][fieldId]; + if (this.__event_listeners_options[type][listener].length == 0) + delete this.__event_listeners_options[type][listener]; + } else { + super_remove_event_listener.call(super_this, type, listener, useCapture); + } + }); + } + + })(); +} + + +//This is a quick polyfill for IE10 that is not exposing CustomEvent as a function. +//From https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill +if ( + typeof document !== "undefined" && + typeof window !== "undefined" && + typeof Event !== "undefined" && + typeof CustomEvent !== "function" +) { + CustomEvent = function CustomEvent (event, params) { // jshint ignore:line + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + }; + + CustomEvent.prototype = Event.prototype; + window.CustomEvent = CustomEvent; +} + + + +// jshint -W015 +/* This is to handle browsers that have TouchEvents but don't have the global constructor function Touch */ +if ( + typeof document !== "undefined" && + typeof window !== "undefined" && + typeof Touch === "undefined" && + "ontouchstart" in window +) { + Touch = function Touch() {}; // jshint ignore:line + + (function () { + var onFirstTouchstart; + document.addEventListener("touchstart", onFirstTouchstart = function onFirstTouchstart(event) { + window.Touch = event.touches[0].constructor; + if (document.nativeRemoveEventListener) { + document.nativeRemoveEventListener("touchstart", onFirstTouchstart, true); + } else { + document.removeEventListener("touchstart", onFirstTouchstart, true); + } + if (defaultEventManager && defaultEventManager.isStoringPointerEvents) { + defaultEventManager.isStoringPointerEvents = false; + defaultEventManager.isStoringPointerEvents = true; + } + }, true); + })(); + + window.Touch = Touch; +} + + +var _PointerVelocity = Montage.specialize({ + _identifier: { + enumerable: false, + writable: true, + value: null + }, + initWithIdentifier: { + value: function (identifier) { + this._identifier = identifier; + return this; + } + }, + clearCache: { + value: function () { + this._data = this._x = this._y = this._speed = this._angle = null; + return this; + } + }, + _data: { + enumerable: false, + writable: true, + value: null + }, + _x: { + enumerable: false, + writable: true, + value: null + }, + _y: { + enumerable: false, + writable: true, + value: null + }, + _speed: { + enumerable: false, + writable: true, + value: null + }, + _angle: { + enumerable: false, + writable: true, + value: null + }, + x: { + get: function () { + if (this._x === null) { + if (this._data === null) { + this._data = defaultEventManager._getPointerVelocityData(this._identifier); + } + this._x = defaultEventManager._calculatePointerVelocity(this._data.time, this._data.x); + } + return this._x; + }, + set: function () { + } + }, + y: { + get: function () { + if (this._y === null) { + if (this._data === null) { + this._data = defaultEventManager._getPointerVelocityData(this._identifier); + } + this._y = defaultEventManager._calculatePointerVelocity(this._data.time, this._data.y); + } + return this._y; + }, + set: function () { + } + }, + speed: { + get: function () { + if (this._speed === null) { + this._speed = Math.sqrt(this.x * this.x + this.y * this.y); + } + return this._speed; + }, + set: function () { + } + }, + angle: { + get: function () { + if (this._angle === null) { + this._angle = Math.atan2(this.y, this.x); + } + return this._angle; + }, + set: function () { + } + } +}); + + +var _PointerStorageMemoryEntry = Montage.specialize({ + constructor: { + value: function (identifier) { + this.data = new Array(32); + this.velocity = {velocity: (new _PointerVelocity()).initWithIdentifier(identifier)}; + return this; + } + }, + data: { + enumerable: false, + writable: true, + value: null + }, + size: { + enumerable: false, + writable: true, + value: 0 + }, + pos: { + enumerable: false, + writable: true, + value: 0 + }, + velocity: { + enumerable: false, + writable: true, + value: 0 + } + +}); + +var _StoredEvent = Montage.specialize({ + constructor: { + value: function (clientX, clientY, timeStamp) { + this.clientX = clientX; + this.clientY = clientY; + this.timeStamp = timeStamp; + return this; + } + }, + clientX: { + enumerable: false, + writable: true, + value: null + }, + clientY: { + enumerable: false, + writable: true, + value: 0 + }, + timeStamp: { + enumerable: false, + writable: true, + value: 0 + } +}); + + + +/*************************************************************************************************************** + * + * Support for Event Serialiation/Deserialization + * + **************************************************************************************************************/ + +// var _serializeObjectRegisteredEventListenersForPhase = function (serializer, object, registeredEventListeners, eventListenerDescriptors, capture) { +// var i, l, type, listenerRegistrations, listeners, aListener, mapIter; +// mapIter = registeredEventListeners.keys(); + +// while ((type = mapIter.next().value)) { +// listenerRegistrations = registeredEventListeners.get(type); +// listeners = listenerRegistrations && listenerRegistrations.get(object); +// if (Array.isArray(listeners) && listeners.length > 0) { +// for (i = 0, l = listeners.length; i < l; i++) { +// aListener = listeners[i]; +// eventListenerDescriptors.push({ +// type: type, +// listener: serializer.addObjectReference(aListener), +// capture: capture +// }); +// } +// } +// else if (listeners){ +// eventListenerDescriptors.push({ +// type: type, +// listener: serializer.addObjectReference(listeners), +// capture: capture +// }); +// } +// } +// }; + + + +var _serializedListenerEntryForType = function (listenerEntry, eventType, serializer) { + //If entry only contains listener and capture keys, we can steamline to capture only + if(Object.keys(listenerEntry) === 2) { + return { + type: eventType, + listener: serializer.addObjectReference(listenerEntry.listener), + capture: listenerEntry.capture + }; + } else { + var serializedEntry = {}; + Object.assign(serializedEntry,listenerEntry); + delete serializedEntry.listener; + return { + type: eventType, + listener: serializer.addObjectReference(listenerEntry.listener), + options: serializedEntry + }; + } +} + +Serializer.defineSerializationUnit("listeners", function listenersSerializationUnit(serializer, object) { + var eventManager = defaultEventManager, + eventListenerDescriptors = [], + targetEntry = eventManager._targetEventListeners.get(object), + descriptors, + descriptor, + listener; + + // _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredCaptureEventListeners,eventListenerDescriptors,true); + // _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredBubbleEventListeners,eventListenerDescriptors,false); + + targetEntry && targetEntry.forEach(function(targetEntryForEventType/*value*/, eventType/*key*/, map) { + targetEntryForEventType.forEach(function(listenerEntries/*value*/, phase/*key*/, map) { + var i, l, iListenerEntry, iSerializedEntry; + + if (Array.isArray(listenerEntries) && listenerEntries.length > 0) { + for (i = 0, l = listenerEntries.length; i < l; i++) { + eventListenerDescriptors.push(_serializedListenerEntryForType(listenerEntries[i],eventType,serializer)); + } + } + else if (listenerEntries){ + eventListenerDescriptors.push(_serializedListenerEntryForType(listenerEntries,eventType,serializer)); + } + + }); + }); + + + if (eventListenerDescriptors.length > 0) { + return eventListenerDescriptors; + } +}); + +Deserializer.defineDeserializationUnit("listeners", function listenersDeserializationUnit(deserializer, object, listeners) { + for (var i = 0, listener; (listener = listeners[i]); i++) { + if(listener.hasOwnProperty("capture") && typeof listener.capture === "boolean") { + object.addEventListener(listener.type, listener.listener, listener.capture); + } else { + object.addEventListener(listener.type, listener.listener, listener.options); + } + } +}); + + + + +function EventListener(){} + + + + + +/** + * @class EventManager + * @extends Montage + */ +var EventManager = exports.EventManager = Montage.specialize(/** @lends EventManager.prototype # */ { + /** + * @constructs + */ + constructor: { + value: function EventManager () { + this._trackingTouchStartList = new Map(); + this._trackingTouchEndList = new Map(); + this._findActiveTargetMap = new Map(); + this._eventPathForTargetMap = new Map(); + this._claimedPointers = new Map(); + this._registeredCaptureEventListeners = new Map(); + this._registeredBubbleEventListeners = new Map(); + this._observedTarget_byEventType_ = Object.create(null); + this._currentDispatchedTargetListeners = new Map(); + this._elementEventHandlerByElement = new WeakMap(); + this.environment = currentEnvironment; + this.isBrowser = currentEnvironment.isBrowser; + //Faster to invoke on this then a global symbol like Element + this.isBrowser + ? this.isElement = Element.isElement + : undefined; + this._trackingTouchTimeoutIDs = new Map(); + // this._functionType = "function"; + + this._targetEventListeners = new Map(); + /* + _targetEventListeners -> Map( eventType -> map ( + CaptureEventListeners -> [EventListeners{ + listener: {}, + capture: true, + once: true/false, + passive: true/false, + callBack + }] + BubbleEventListeners -> [EventListeners{ + listener: {}, + capture: false, + once: true/false, + passive: true/false, + callBack + + }]) ) + + + when addEventListner is going to be called with a boolean capture, we're going to buil d a listener object with the matching defauls (those might need to change per browser?) + */ + + return this; + } + }, + spliceOne: { + value: function spliceOne(arr, index) { + var len = arr.length; + if (len) { + while (index < len) { + arr[index] = arr[index + 1]; + index++; + } + arr.length--; + } + } + }, + /** + * Utility + * @see ClipboardEvent http://dev.w3.org/2006/webapi/clipops/clipops.html#event-types-and-details + * @see DND http://www.w3.org/TR/2010/WD-html5-20101019/dnd.html + * @see document.implementation.hasFeature("HTMLEvents", "2.0") + * @see DOM2 http://www.w3.org/TR/DOM-Level-2-Events/events.html + * @see DOM3 http://dev.w3.org/2006/webapi/DOM-Level-3-Events/html/DOM3-Events.html + * @see DOM4 http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#events + * @see GECKO https://developer.mozilla.org/en/Gecko-Specific_DOM_Events + * @see MSFT defacto standard + * @see ProgressEvent http://www.w3.org/TR/progress-events/ + * @see TouchEvent http://dvcs.w3.org/hg/webevents/raw-file/tip/touchevents.html + * @see INPUT http://dev.w3.org/html5/spec/common-input-element-apis.html#common-event-behaviors + * @see WEBSOCKETS http://www.w3.org/TR/html5/comms.html + * @see http://www.quirksmode.org/dom/events/index.html + * @see https://developer.mozilla.org/en/DOM/DOM_event_reference + */ + eventDefinitions: { + value: { + abort: {bubbles: false, cancelable: false}, //ProgressEvent, DOM3, //DOM2 does bubble + beforeunload: {bubbles: false}, //MSFT + blur: {bubbles: false, cancelable: false}, //DOM2, DOM3 + change: {bubbles: true, cancelable: false}, //DOM2, INPUT + click: {bubbles: true, cancelable: true}, //DOM3 + close: {bubbles: false, cancelable: false}, //WEBSOCKETS + compositionend: {bubbles: true, cancelable: false}, //DOM3 + compositionstart: {bubbles: true, cancelable: true}, //DOM3 + compositionupdate: {bubbles: true, cancelable: false}, //DOM3 + contextmenu: {bubbles: true, cancelable: true}, //MSFT + copy: {bubbles: true, cancelable: true}, //ClipboardEvent + cut: {bubbles: true, cancelable: true}, //ClipboardEvent + dblclick: {bubbles: true, cancelable: false}, //DOM3 + DOMActivate: {bubbles: true, cancelable: true, deprecated: true}, //DOM2, DOM3 deprecated + DOMContentLoaded: {bubbles: true, cancelable: false}, + DOMMouseScroll: {bubbles: true}, //GECKO + drag: {bubbles: true, cancelable: true}, //DND + dragend: {bubbles: true, cancelable: false}, //DND + dragenter: {bubbles: true, cancelable: true}, //DND + dragleave: {bubbles: true, cancelable: false}, //DND + dragover: {bubbles: true, cancelable: true}, //DND + dragstart: {bubbles: true, cancelable: true}, //DND + drop: {bubbles: true, cancelable: true}, //DND + error: { + bubbles: function (target) { + // error does not bubble when used as a ProgressEvent + // return !(XMLHttpRequest.prototype.isPrototypeOf(target) || + return !(target instanceof XMLHttpRequest || + target instanceof WebSocket || + target.tagName && "VIDEO" === target.tagName.toUpperCase() || + target.tagName && "AUDIO" === target.tagName.toUpperCase()); + }, + cancelable: false + }, //DOM2, DOM3, ProgressEvent + focus: {bubbles: false, cancelable: false}, //DOM2, DOM3 + focusin: {bubbles: true, cancelable: false}, //DOM3 + focusout: {bubbles: true, cancelable: false}, //DOM3 + input: {bubbles: true, cancelable: false}, // INPUT + invalid: {bubbles: false, cancelable: true}, // INPUT + keydown: {bubbles: true, cancelable: false}, //DOM3 + keypress: {bubbles: true, cancelable: false}, //DOM3 + keyup: {bubbles: true, cancelable: false}, //DOM3 + load: {bubbles: false, cancelable: false}, //ProgressEvent, DOM2, DOM3 + loadend: {bubbles: false, cancelable: false}, //ProgressEvent + loadstart: {bubbles: false, cancelable: false}, //ProgressEvent + message: {bubbles: false, cancelable: false}, //WEBSOCKETS + mousedown: {bubbles: true, cancelable: true}, //DOM3 + mouseenter: {bubbles: false, cancelable: false}, //DOM3 + mouseleave: {bubbles: false, cancelable: false}, //DOM3 + mousemove: {bubbles: true, cancelable: true}, //DOM3 + mouseout: {bubbles: true, cancelable: true}, //DOM3 + mouseover: {bubbles: true, cancelable: true}, //DOM3 + mouseup: {bubbles: true, cancelable: true}, //DOM3 + mousewheel: {bubbles: true}, + offline: {bubbles: false, cancelable: false}, //DOM3 + online: {bubbles: false, cancelable: false}, //DOM3 + open: {bubbles: false, cancelable: false}, //DOM3 + orientationchange: {bubbles: false}, + paste: {bubbles: true, cancelable: true}, //ClipboardEvent + progress: {bubbles: false, cancelable: false}, //ProgressEvent + reset: {bubbles: true, cancelable: false}, //DOM2 + resize: {bubbles: false, cancelable: false}, //DOM2 bubbles, DOM3 + + scroll: { + bubbles: function (target) { + return /*isDocument*/!!target.defaultView; + }, + cancelable: false + }, //DOM2, DOM3 When dispatched on Document element must bubble to defaultView object + + select: {bubbles: true, cancelable: false}, //DOM2, DOM3 + + submit: {bubbles: true, cancelable: true}, //DOM2 + timeout: {bubbles: false, cancelable: false}, //DOM2 + touchcancel: {bubbles: true, cancelable: false}, //TouchEvent + touchend: {bubbles: true, cancelable: true}, //TouchEvent + touchmove: {bubbles: true, cancelable: true}, //TouchEvent + touchstart: {bubbles: true, cancelable: true}, //TouchEvent + unload: {bubbles: false, cancelable: false}, //DOM2, DOM3 + visibilitychange: { + bubbles: true, + cancelable: false, + type: (typeof document !== "undefined") + ? (typeof document.msHidden !== "undefined") + ? "msvisibilitychange" + : (typeof document.webkitHidden !== "undefined") + ? "webkitvisibilitychange" + : undefined + : undefined + }, //DOM2, DOM3 + webkitvisibilitychange: { + bubbles: true, + cancelable: false, + type: "visibilitychange" + }, + msvisibilitychange: { + bubbles: true, + cancelable: false, + type: "visibilitychange" + }, //DOM2, DOM3 + wheel: {bubbles: true, cancelable: true}, //DOM3 + pointerdown: {bubbles: true, cancelable: true}, //PointerEvent + pointerup: {bubbles: true, cancelable: true}, //PointerEvent + pointerenter: {bubbles: false, cancelable: true}, //PointerEvent + pointercancel: {bubbles: true, cancelable: true}, //PointerEvent + pointerout: {bubbles: true, cancelable: true}, //PointerEvent + pointerover: {bubbles: true, cancelable: true}, //PointerEvent + pointerleave: {bubbles: false, cancelable: true}, //PointerEvent + pointermove: {bubbles: true, cancelable: true}, //PointerEvent + MSPointerDown: {bubbles: true, cancelable: true}, //MSPointerEvent + MSPointerMove: {bubbles: true, cancelable: true}, //PointerEvent + MSPointerUp: {bubbles: true, cancelable: true}, //MSPointerEvent + MSPointerOver: {bubbles: true, cancelable: true}, //MSPointerEvent + MSPointerOut: {bubbles: true, cancelable: true}, //MSPointerEvent + MSPointerHover: {bubbles: true, cancelable: true}//MSPointerEvent + + } + }, + + /** + * @private + */ + _DOMPasteboardElement: { + value: null, + enumerable: false + }, + + /** + * @property {string} value + * @private + */ + _delegate: { + value: null, + enumerable: false + }, + + /** + * @returns {string} + * @param {string} + * @default null + */ + delegate: { + enumerable: false, + get: function () { + return this._delegate; + }, + set: function (delegate) { + this._delegate = delegate; + } + }, + + /** + * @property {Application} value + * @private + */ + _application: { + value: null, + enumerable: false + }, + + /** + * The application object associated with the event manager. + * @returns {Application} + * @param {Application} + * @default null + * + * @todo if this changes...we probably need to unregister all the windows + * we know about and frankly probably the components too + */ + application: { + enumerable: false, + get: function () { + return this._application; + }, + set: function (application) { + this._application = application; + //To avoid circular depenencies, we set it on environment + this.environment.application = application; + } + }, + + // Dictionary keyed by event types with the collection of handlers per event type + // This dictates why the event manager observes events of a particular type + + /** + * All windows this event Manager may be listening to + * + * @property {Array} value + * @private + */ + _registeredWindows: { + value: null, + enumerable: false + }, + + /** + * @property {Object} value + * @private + */ + _windowsAwaitingFinalRegistration: { + value: (new WeakMap), + enumerable: false + }, + + // Initialization + + /** + * @function + * @param {external:window} aWindow + * @returns {EventManager} + */ + initWithWindow: { + enumerable: false, + value: function (aWindow) { + if (!!this._registeredWindows) { + throw "EventManager has already been initialized"; + } + + // TODO do we also complain if no window is given? + // Technically we don't need one until we start listening for events + this.registerWindow(aWindow); + return this; + } + }, + /** + * @function + * @param {external:window} aWindow + */ + registerWindow: { + enumerable: false, + value: function (aWindow) { + + if (aWindow.defaultEventManager && aWindow.defaultEventManager !== this) { + throw Error("EventManager cannot register a window already registered to another EventManager"); + } + + if (this._registeredWindows && this._registeredWindows.indexOf(aWindow) >= 0) { + throw new Error("EventManager cannot register a window more than once"); + } + + if (!this._registeredWindows) { + this._registeredWindows = []; + } + + if (this._windowsAwaitingFinalRegistration.has(aWindow)) { + return; + } + + // Setup the window as much as possible now without knowing whether + // the DOM is ready or not + + // Keep a reference to the original listener functions + + // Note I think it may be implementation specific how these are implemented + // so I'd rather preserve any native optimizations a browser has for + // adding listeners to the document versus and element etc. + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.nativeAddEventListener = aWindow.EventTarget.prototype.addEventListener; + } + + aWindow.Element.prototype.nativeAddEventListener = aWindow.Element.prototype.addEventListener; + Object.defineProperty(aWindow, "nativeAddEventListener", { + configurable: true, + value: aWindow.addEventListener + }); + + aWindow.document.nativeAddEventListener = aWindow.document.addEventListener; + aWindow.XMLHttpRequest.prototype.nativeAddEventListener = aWindow.XMLHttpRequest.prototype.addEventListener; + + if (aWindow.DocumentFragment) { + aWindow.DocumentFragment.prototype.nativeAddEventListener = aWindow.DocumentFragment.prototype.addEventListener; + } + + if (aWindow.ShadowRoot) { + aWindow.ShadowRoot.prototype.nativeAddEventListener = aWindow.ShadowRoot.prototype.addEventListener; + } + + if (aWindow.Worker) { + aWindow.Worker.prototype.nativeAddEventListener = aWindow.Worker.prototype.addEventListener; + } + if (aWindow.MediaController) { + aWindow.MediaController.prototype.nativeAddEventListener = aWindow.MediaController.prototype.addEventListener; + } + + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.nativeRemoveEventListener = aWindow.EventTarget.prototype.removeEventListener; + } + aWindow.Element.prototype.nativeRemoveEventListener = aWindow.Element.prototype.removeEventListener; + Object.defineProperty(aWindow, "nativeRemoveEventListener", { + configurable: true, + value: aWindow.removeEventListener + }); + + aWindow.document.nativeRemoveEventListener = aWindow.document.removeEventListener; + aWindow.XMLHttpRequest.prototype.nativeRemoveEventListener = aWindow.XMLHttpRequest.prototype.removeEventListener; + + if (aWindow.DocumentFragment) { + aWindow.DocumentFragment.prototype.nativeRemoveEventListener = aWindow.DocumentFragment.prototype.removeEventListener; + } + + if (aWindow.ShadowRoot) { + aWindow.ShadowRoot.prototype.nativeRemoveEventListener = aWindow.ShadowRoot.prototype.removeEventListener; + } + + if (aWindow.Worker) { + aWindow.Worker.prototype.nativeRemoveEventListener = aWindow.Worker.prototype.removeEventListener; + } + if (aWindow.MediaController) { + aWindow.MediaController.prototype.nativeRemoveEventListener = aWindow.MediaController.prototype.removeEventListener; + } + + // Redefine listener functions + Object.defineProperty(aWindow, "addEventListener", { + configurable: true, + value: (aWindow.XMLHttpRequest.prototype.addEventListener = + aWindow.Element.prototype.addEventListener = + aWindow.document.addEventListener = + function addEventListener(eventType, listener, optionsOrUseCapture) { + //this.nativeAddEventListener(eventType, listener, optionsOrUseCapture); + aWindow.defaultEventManager.registerTargetEventListener(this, eventType, listener, optionsOrUseCapture); + }) + }); + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.addEventListener = aWindow.addEventListener; + } + + if (aWindow.Worker) { + aWindow.Worker.prototype.addEventListener = aWindow.addEventListener; + } + if (aWindow.MediaController) { + aWindow.MediaController.prototype.addEventListener = aWindow.addEventListener; + } + + Object.defineProperty(aWindow, "removeEventListener", { + configurable: true, + value: ( + aWindow.XMLHttpRequest.prototype.removeEventListener = + aWindow.Element.prototype.removeEventListener = + aWindow.document.removeEventListener = + function removeEventListener(eventType, listener, optionsOrUseCapture) { + //this.nativeRemoveEventListener(eventType, listener, optionsOrUseCapture); + if(aWindow.defaultEventManager) { + return aWindow.defaultEventManager.unregisterTargetEventListener(this, eventType, listener, optionsOrUseCapture); + } + }) + }); + + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.removeEventListener = aWindow.removeEventListener; + } + + if (aWindow.Worker) { + aWindow.Worker.prototype.removeEventListener = aWindow.removeEventListener; + } + if (aWindow.MediaController) { + aWindow.MediaController.prototype.removeEventListener = aWindow.removeEventListener; + } + + // In some browsers (Firefox) each element has their own addEventLister/removeEventListener + // Methodology to find all elements found in Chainvas (now mostly gone from this) + if (aWindow.HTMLDivElement.prototype.addEventListener !== aWindow.Element.prototype.nativeAddEventListener) { + if (aWindow.HTMLElement && + 'addEventListener' in aWindow.HTMLElement.prototype + ) { + var candidates = Object.getOwnPropertyNames(aWindow), + candidate, candidatePrototype, + i = 0, candidatesLength = candidates.length; + for (i; i < candidatesLength; i++) { + candidate = candidates[i]; + if (candidate.match(/^HTML\w*Element$/) && typeof candidate === "function") { + candidatePrototype = candidate.prototype; + candidatePrototype.nativeAddEventListener = candidatePrototype.addEventListener; + candidatePrototype.addEventListener = aWindow.Element.prototype.addEventListener; + candidatePrototype.nativeRemoveEventListener = candidatePrototype.removeEventListener; + candidatePrototype.removeEventListener = aWindow.Element.prototype.removeEventListener; + } + } + } + } + + /** + * The component instance directly associated with the specified element. + * + * @member external:Element#component + */ + Montage.defineProperty(aWindow.Element.prototype, "component", { + get: function () { + return defaultEventManager._elementEventHandlerByElement.get(this); + }, + enumerable: false + }); + + /** + * @namespace EventManager + * @instance + * @global + */ + defaultEventManager = aWindow.defaultEventManager = exports.defaultEventManager = this; + this._registeredWindows.push(aWindow); + + this._windowsAwaitingFinalRegistration.set(aWindow,aWindow); + + // Some registration demands the window's dom be accessible + // only finalize registration when that's true + if (/loaded|complete|interactive/.test(aWindow.document.readyState)) { + this._finalizeWindowRegistration(aWindow); + } else { + aWindow.document.addEventListener("DOMContentLoaded", this, true); + } + + + + this._evaluateShouldDispatchEventCondition(); + + + } + }, + + _finalizeWindowRegistration: { + enumerable: false, + value: function (aWindow) { + + if (!this._windowsAwaitingFinalRegistration.has(aWindow)) { + throw "EventManager wasn't expecting to register this window"; + } + + this._windowsAwaitingFinalRegistration.delete(aWindow); + + this._listenToWindow(aWindow); + // TODO uninstall DOMContentLoaded listener if all windows finalized + } + }, + /** + * @function + * @param {external:window} aWindow + */ + unregisterWindow: { + enumerable: false, + value: function (aWindow) { + if (this._registeredWindows.indexOf(aWindow) < 0) { + throw "EventManager cannot unregister an unregistered window"; + } + + this._registeredWindows = this._registeredWindows.filter(function (element) { + return (aWindow !== element); + }); + + delete aWindow.defaultEventManager; + + // Restore existing listener functions + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.addEventListener = aWindow.EventTarget.prototype.nativeAddEventListener; + } + aWindow.Element.prototype.addEventListener = aWindow.Element.prototype.nativeAddEventListener; + Object.defineProperty(aWindow, "addEventListener", { + configurable: true, + value: aWindow.nativeAddEventListener + }); + + aWindow.document.addEventListener = aWindow.document.nativeAddEventListener; + aWindow.XMLHttpRequest.prototype.addEventListener = aWindow.XMLHttpRequest.prototype.nativeAddEventListener; + if (aWindow.Worker) { + aWindow.Worker.prototype.addEventListener = aWindow.Worker.prototype.nativeAddEventListener; + } + + if (aWindow.EventTarget) { + aWindow.EventTarget.prototype.removeEventListener = aWindow.EventTarget.prototype.nativeRemoveEventListener; + } + + aWindow.Element.prototype.removeEventListener = aWindow.Element.prototype.nativeRemoveEventListener; + Object.defineProperty(aWindow, "removeEventListener", { + configurable: true, + value: aWindow.nativeRemoveEventListener + }); + + aWindow.document.removeEventListener = aWindow.document.nativeRemoveEventListener; + aWindow.XMLHttpRequest.prototype.removeEventListener = aWindow.XMLHttpRequest.prototype.nativeRemoveEventListener; + if (aWindow.Worker) { + aWindow.Worker.prototype.removeEventListener = aWindow.Worker.prototype.nativeRemoveEventListener; + } + + // In some browsers (Firefox) each element has their own addEventLister/removeEventListener + // Methodology to find all elements found in Chainvas + if (aWindow.HTMLDivElement.prototype.nativeAddEventListener !== aWindow.Element.prototype.addEventListener) { + if (aWindow.HTMLElement && + 'addEventListener' in aWindow.HTMLElement.prototype && + aWindow.Components && + aWindow.Components.interfaces + ) { + var candidate, candidatePrototype; + + for (candidate in Components.interfaces) { + if (candidate.match(/^nsIDOMHTML\w*Element$/)) { + candidate = candidate.replace(/^nsIDOM/, ''); + if ((candidate = window[candidate])) { + candidatePrototype = candidate.prototype; + candidatePrototype.addEventListener = candidatePrototype.nativeAddEventListener; + delete candidatePrototype.nativeAddEventListener; + candidatePrototype.removeEventListener = candidatePrototype.nativeRemoveEventListener; + delete candidatePrototype.nativeRemoveEventListener; + } + } + } + } + } + + // Delete our references + if (aWindow.EventTarget) { + delete aWindow.EventTarget.prototype.nativeAddEventListener; + } + + delete aWindow.Element.prototype.nativeAddEventListener; + delete aWindow.nativeAddEventListener; + + delete aWindow.document.nativeAddEventListener; + delete aWindow.XMLHttpRequest.prototype.nativeAddEventListener; + if (aWindow.Worker) { + delete aWindow.Worker.prototype.nativeAddEventListener; + } + + if (aWindow.EventTarget) { + delete aWindow.EventTarget.prototype.nativeRemoveEventListener; + } + delete aWindow.Element.prototype.nativeRemoveEventListener; + delete aWindow.nativeRemoveEventListener; + + delete aWindow.document.nativeRemoveEventListener; + delete aWindow.XMLHttpRequest.prototype.nativeRemoveEventListener; + if (aWindow.Worker) { + delete aWindow.Worker.prototype.nativeRemoveEventListener; + } + + delete aWindow.Element.prototype.component; + + this._stopListeningToWindow(aWindow); + } + }, + + /** + * @function + */ + unregisterWindows: { + enumerable: false, + value: function () { + this._registeredWindows.forEach(this.unregisterWindow); + } + }, + + // Event Handler Registration + + /** + * Registered event listeners. + * + * @example + * ```json * ``` + * + * @property {Listeners} value + * @default {} + */ + registeredEventListeners: { + enumerable: false, + value: {} + }, + + /** + * Returns a dictionary of all listeners registered for the specified eventType, + * regardless of the target being observed. + * + * @function + * @param {Event} eventType The event type. + * @returns null || listeners + */ + // registeredEventListenersForEventType_: { + // value: function (eventType) { + // var captureRegistration = this._registeredCaptureEventListeners.get(eventType), + // bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), + // result = null; + + // if (captureRegistration) { + // captureRegistration.forEach(function(listeners, target, map) { + // if (listeners && listeners.length > 0) { + // result = result || []; + // listeners.forEach(function(aListener) { + // result.push(aListener); + // }); + // } + // }); + // } + + // if (bubbleRegistration) { + // bubbleRegistration.forEach(function(listeners, target, map) { + // if (listeners && listeners.length > 0) { + // result = result || []; + // listeners.forEach(function(aListener) { + // result.push(aListener); + // }); + // } + // }); + // } + + // return result; + // } + // }, + + registeredEventListenersForEventType_: { + value: function (eventType) { + var result = null, + self = this; + this._targetEventListeners.forEach(function(targetEntry/*value*/, target/*key*/, map) { + + var forEachResult = self.registeredEventListenersForEventType_onTarget_(eventType, target); + if(forEachResult) { + if(!Array.isArray(forEachResult)) { + (result || (result = [])).push(forEachResult); + } else { + Array.prototype.push.apply((result || (result = [])), forEachResult); + } + } + + }); + return result; + } + }, + + + + /** + * Returns the list of all listeners registered for + * the specified eventType on the specified target, regardless of the phase. + * + * @function + * @param {Event} eventType - The event type. + * @param {Event} target - The event target. + * @returns {?ActionEventListener} + */ + // registeredEventListenersForEventType_onTarget_: { + // enumerable: false, + // value: function (eventType, target) { + + // var captureRegistration = this._registeredCaptureEventListeners.get(eventType), + // bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), + // listeners, + // result = null; + + // if (!eventType || !target || (!captureRegistration && !bubbleRegistration)) { + // return null; + // } else { + // listeners = captureRegistration ? captureRegistration.get(target) : null; + // if (listeners) { + // if (!result) { + // result = listeners; + // } + // } + // listeners = bubbleRegistration ? bubbleRegistration.get(target) : null; + // if (listeners) { + // if (!result) { + // result = listeners; + // } else { + // result = result.union(listeners); + // } + // } + // return result; + // } + // } + // }, + + registeredEventListenersForEventType_onTarget_: { + enumerable: false, + value: function (eventType, target) { + if (!eventType || !target) { + return null; + } + + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry && targetEntry.get(eventType), + captureListenerEntries = targetEntryForEventType && targetEntryForEventType.get(Event_CAPTURING_PHASE), + bubbleListenerEntries = targetEntryForEventType && targetEntryForEventType.get(Event_BUBBLING_PHASE), + result = null; + + if(captureListenerEntries || bubbleListenerEntries) { + if(captureListenerEntries) { + if(!Array.isArray(captureListenerEntries)) { + result = captureListenerEntries.listener; + } + else { + result = []; + Array.prototype.push.apply(result, captureListenerEntries.map(this._listenerEntryListenerMapper)); + } + } + if(bubbleListenerEntries) { + if(!Array.isArray(bubbleListenerEntries)) { + if(!result) { + result = bubbleListenerEntries.listener; + } else if(!Array.isArray(result)) { + result = [result,bubbleListenerEntries.listener] + } else { + result.push(bubbleListenerEntries.listener) + } + } + else { + if(!result) { + result = []; + } + Array.prototype.push.apply(result, bubbleListenerEntries.map(this._listenerEntryListenerMapper)); + } + } + } + + return result; + } + }, + _listenerEntryListenerMapper: { + value: function(listenerEntry) { + return listenerEntry.listener; + } + }, + + /** + * Returns all listeners registered for + * the specified eventType on the specified target and specified phase. + * + * @function + * @param {Event} eventType - The event type. + * @param {Event} target - The event target. + * @returns {?ActionEventListener} + */ + // registeredEventListenersForEventType_onTarget_phase_: { + // enumerable: false, + // value: function (eventType, target, capture) { + // if (!target) { + // return null; + // } + // return this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, target, (capture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners)); + // } + // }, + registeredEventListenersForEventType_onTarget_phase_: { + enumerable: false, + value: function (eventType, target, capture) { + if (!target) { + return null; + } + return this._registeredEventListenersOnTarget_eventType_eventPhase(target, eventType, (capture ? Event_CAPTURING_PHASE : Event_BUBBLING_PHASE)); + } + }, + + _registeredEventListenersForEventType_onTarget_registeredEventListeners_: { + value: function (eventType, target, registeredEventListeners) { + + // 0.02224230716159459 on Samsung Galaxy Tab3 7" + var result = registeredEventListeners.get(eventType); + return result ? result.get(target) : null; + }, + enumerable: false + }, + + _registeredEventListenersOnTarget_eventType_eventPhase: { + value: function (target, eventType, eventPhase) { + + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null, + targetEntryForEventTypeListeners = targetEntryForEventType + ? eventPhase === Event_CAPTURING_PHASE + ? targetEntryForEventType.get(Event_CAPTURING_PHASE) + : targetEntryForEventType.get(Event_BUBBLING_PHASE) + : null; + + return targetEntryForEventTypeListeners || null; + }, + enumerable: false + }, + + + /** + * Returns the dictionary of all listeners registered on + * the specified target, keyed by eventType. + * + * @function + * @param {Event} target The event target. + * @returns observedEventListeners + */ + registeredEventListenersOnTarget_: { + value: function (target) { + + var eventType, + eventRegistration, + _registeredCaptureEventListeners = this._registeredCaptureEventListeners, + _registeredBubbleEventListeners = this._registeredBubbleEventListeners, + observedEventListeners = [], + mapIter; + + mapIter = _registeredCaptureEventListeners.keys(); + while ((eventType = mapIter.next().value)) { + eventRegistration = _registeredCaptureEventListeners.get(eventType); + if (eventRegistration.has(target)) { + observedEventListeners.push(eventType); + } + } + + mapIter = _registeredBubbleEventListeners.keys(); + while ((eventType = mapIter.next().value)) { + eventRegistration = _registeredBubbleEventListeners.get(eventType); + if (eventRegistration.has(target)) { + observedEventListeners.push(eventType); + } + } + + + return observedEventListeners; + } + }, + + /** + * This adds the listener to the definitive collection of + * what targets are being observed for what eventTypes by whom and in what phases. + * This collection maintained by the EventManager is used throughout + * the discovery and distribution steps of the event handling system. + * + * @function + * @param {Event} target - The event target. + * @param {Event} eventType - The event type. + * @param {Event} listener - The event listener. + * @param {Event} useCapture - The event capture. + * @returns returnResult + */ + _registeredCaptureEventListeners: { + value:null + }, + + _registeredBubbleEventListeners: { + value:null + }, + + _indexOfRegisteredListener: { + enumerable: false, + value: function indexOfRegisteredListener(targetEntryForEventTypeListeners, listener, listenerEntry) { + var i=0, countI = targetEntryForEventTypeListeners.length; + + if(listenerEntry) { + + for(;i { + for (let i = 0, length = entries.length; i < length; i++) { + // Assuming ChangeEvent is a class or constructor available in this scope + var changeEvent = new ChangeEvent(); + changeEvent.target = entries[i].target; + changeEvent.key = "size"; + changeEvent.keyValue = entries[i]; // The entry object contains detailed resize info + this.handleEvent(changeEvent); // 'this' correctly refers to YourClass instance + } + }); + } + return this.__resizeObserver; + } + }, + + _isDOMTargetChangeListener: { + enumerable: false, + value: function _isDOMTargetChangeListener(target, eventType, optionsOrUseCapture) { + return (eventType === "change" && this.isBrowser && optionsOrUseCapture?.hasOwnProperty("size") && + (target instanceof Element || (typeof SVGElement !== "undefined" && target instanceof SVGElement))); + } + }, + + registerTargetEventListener: { + enumerable: false, + value: function registerTargetEventListener(target, eventType, listener, optionsOrUseCapture) { + var listenerOptions = typeof optionsOrUseCapture === "object" ? optionsOrUseCapture : {capture: !!optionsOrUseCapture}, + targetEntry = this._targetEventListeners.get(target) || (this._targetEventListeners.set(target, (targetEntry = new Map())) && targetEntry), + targetEntryForEventType = targetEntry.get(eventType) || (targetEntry.set(eventType, (targetEntryForEventType = new Map())) && targetEntryForEventType), + isNewTarget = false, + //For the first listener and for perf optimization we don't create an array. We do that starting with the second listener, unless for some reason the listener is an Array?? + targetEntryForEventTypeListeners = listenerOptions.capture + ? targetEntryForEventType.get(Event_CAPTURING_PHASE) || (targetEntryForEventType.set(Event_CAPTURING_PHASE, (targetEntryForEventTypeListeners = Array.isArray(listenerOptions) ? [listenerOptions] : listenerOptions)) && targetEntryForEventTypeListeners) + : targetEntryForEventType.get(Event_BUBBLING_PHASE) || (targetEntryForEventType.set(Event_BUBBLING_PHASE, (targetEntryForEventTypeListeners = Array.isArray(listenerOptions) ? [listenerOptions] : listenerOptions)) && targetEntryForEventTypeListeners); + + // if(eventType === "pointerdown" || eventType === "pointerup" || eventType.indexOf("press") !== -1) { + if(typeof optionsOrUseCapture === "object") { + + if(optionsOrUseCapture.passive) { + // console.log(target,"registerTargetEventListener "+eventType," listener:",listener, "optionsOrUseCapture:",optionsOrUseCapture); + } + //trigger passive: + optionsOrUseCapture.passive; + + } + // else { + // console.log(target.toString(),"registerTargetEventListener "+eventType); + + // } + // } + + if(this._isDOMTargetChangeListener(target, eventType, optionsOrUseCapture)) { + this._resizeObserver.observe(target, {box: optionsOrUseCapture?.size?.box}); + } + + listenerOptions.listener = listener; + + if(targetEntryForEventTypeListeners) { + if(Array.isArray(targetEntryForEventTypeListeners)) { + if(Array.isArray(listener) && targetEntryForEventTypeListeners.length === 1) { + isNewTarget = true; + } + if(this._indexOfRegisteredListener(targetEntryForEventTypeListeners,listener) === -1) { + //We need to make sure we don't add it twice: + targetEntryForEventTypeListeners.push(listenerOptions); + } + } else if(targetEntryForEventTypeListeners.listener !== listener) { + targetEntryForEventType.set(listenerOptions.capture ? Event_CAPTURING_PHASE : Event_BUBBLING_PHASE,[targetEntryForEventTypeListeners, listenerOptions]); + } else { + isNewTarget = true; + } + } + //console.log("targetEntryForEventType",targetEntryForEventType); + + //We'll check/cache the callbacks when we first dispatch + + if (isNewTarget && typeof target.nativeAddEventListener === "function") { + //if (targetEntryForEventType.size === 1 && typeof target.nativeAddEventListener === "function") { + this._observeTarget_forEventType_(target, eventType, listenerOptions); + } + } + }, + + unregisterTargetEventListener: { + enumerable: false, + value: function unregisterTargetEventListener(target, eventType, listener, optionsOrUseCapture) { + // if(typeof optionsOrUseCapture === "object") { + // console.log(target,"unregisterTargetEventListener "+eventType," listener:",listener, "optionsOrUseCapture:",optionsOrUseCapture); + // } + + var listenerOptions = typeof optionsOrUseCapture === "object" ? optionsOrUseCapture : {capture: !!optionsOrUseCapture}, + targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null, + targetEntryForEventTypeListeners = targetEntryForEventType + ? listenerOptions.capture + ? targetEntryForEventType.get(Event_CAPTURING_PHASE) + : targetEntryForEventType.get(Event_BUBBLING_PHASE) + : null, + targetEntryForEventTypeOtherPhaseListeners = targetEntryForEventType + ? listenerOptions.capture + ? targetEntryForEventType.get(Event_BUBBLING_PHASE) + : targetEntryForEventType.get(Event_CAPTURING_PHASE) + : null, + /* Only if options is passed as an array car we safely assume it's the objects we stored and keep track on, which helps us find it faster */ + registeredListenerRecordIndex = Array.isArray(targetEntryForEventTypeListeners) + ? this._indexOfRegisteredListener(targetEntryForEventTypeListeners,listener, (optionsOrUseCapture === listenerOptions ? listenerOptions : null)) + : -1; + + if(registeredListenerRecordIndex !== -1) { + if (this._currentDispatchedTargetListeners.has(targetEntryForEventTypeListeners)) { + var indexes = this._currentDispatchedTargetListeners.get(targetEntryForEventTypeListeners); + if (!indexes) { + this._currentDispatchedTargetListeners.set(targetEntryForEventTypeListeners,(indexes = [])); + } + indexes.push(registeredListenerRecordIndex); + + } else { + this.spliceOne(targetEntryForEventTypeListeners,registeredListenerRecordIndex); + } + //If it's empty after, we remove + if(targetEntryForEventTypeListeners.length === 0) { + listenerOptions.capture + ? targetEntryForEventType.delete(Event_CAPTURING_PHASE) + : targetEntryForEventType.delete(Event_BUBBLING_PHASE) + } + + } else if(targetEntryForEventTypeListeners) { + //We have only one listener, we remove the whole entry + listenerOptions.capture + ? targetEntryForEventType.delete(Event_CAPTURING_PHASE) + : targetEntryForEventType.delete(Event_BUBBLING_PHASE) + } + if(this._isDOMTargetChangeListener(target, eventType, optionsOrUseCapture)) { + this._resizeObserver.unobserve(target); + } + + this._stopObservingTargeForEventTypeIfNeeded(target, eventType, targetEntryForEventType); + // if(targetEntryForEventType && !targetEntryForEventType.get(Event_CAPTURING_PHASE) && !targetEntryForEventType.get(Event_BUBBLING_PHASE)) { + // // If no targets for this eventType; stop observing this event + // this._stopObservingTarget_forEventType_(target, eventType); + // } + } + }, + + _stopObservingTargeForEventTypeIfNeeded: { + value: function(target, eventType, _targetEntryForEventType) { + var targetEntry, + targetEntryForEventType = _targetEntryForEventType || ((targetEntry = this._targetEventListeners.get(target)) ? targetEntry.get(eventType) : null); + + if(targetEntryForEventType && !targetEntryForEventType.get(Event_CAPTURING_PHASE) && !targetEntryForEventType.get(Event_BUBBLING_PHASE)) { + // If no targets for this eventType; stop observing this event + this._stopObservingTarget_forEventType_(target, eventType); + } + + } + }, + + registerEventListener: { + enumerable: false, + value: function registerEventListener(target, eventType, listener, optionsOrUseCapture) { + var capture = typeof optionsOrUseCapture === "object" ? optionsOrUseCapture.capture : !!optionsOrUseCapture; + this._registerEventListener(target, eventType, listener, capture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners); + } + }, + _registerEventListener: { + enumerable: false, + value: function _registerEventListener(target, eventType, listener, registeredEventListeners) { + var eventTypeRegistration = registeredEventListeners.get(eventType), + isNewTarget = false, + returnResult = false, + listeners; + + if (!eventTypeRegistration) { + // First time this eventType has been requested + registeredEventListeners.set(eventType, (eventTypeRegistration = new Map())); + // listeners = [listener]; + // eventTypeRegistration.set(target,listeners); + eventTypeRegistration.set(target,listener); + + isNewTarget = true; + returnResult = true; + } else { + + // Or, the event type was already observed; install this new listener (or at least any new parts) + if (!eventTypeRegistration.has(target)) { + // listeners = []; + // eventTypeRegistration.set(target,listeners); + eventTypeRegistration.set(target,listener); + + isNewTarget = true; + } + else { + listeners = eventTypeRegistration.get(target); + if (Array.isArray(listeners)) { + if (listeners.indexOf(listener) !== -1) { + returnResult = true; + } else { + listeners.push(listener); + returnResult = true; + } + } else { + if (listeners !== listener) { + listeners = [listeners,listener]; + eventTypeRegistration.set(target,listeners); + } + + returnResult = true; + } + } + } + + if (isNewTarget && typeof target.nativeAddEventListener === "function") { + this._observeTarget_forEventType_(target, eventType); + } + + return returnResult; + } + }, + /** + * This unregisters the listener. + * + * @function + * @param {Event} target - The event target. + * @param {Event} eventType - The event type. + * @param {Event} listener - The event listener. + * @param {Event} useCapture - The event capture. + */ + unregisterEventListener: { + enumerable: false, + value: function unregisterEventListener(target, eventType, listener, useCapture) { + //console.log("EventManager.unregisterEventListener", target, eventType, listener, useCapture); + + return useCapture ? + this._unregisterEventListener(target, eventType, listener, this._registeredCaptureEventListeners, this._registeredBubbleEventListeners) : + this._unregisterEventListener(target, eventType, listener, this._registeredBubbleEventListeners, this._registeredCaptureEventListeners); + } + }, + _unregisterEventListener: { + enumerable: false, + value: function _unregisterEventListener (target, eventType, listener, registeredEventListeners, otherPhaseRegisteredEventListeners) { + + + var eventTypeRegistration = registeredEventListeners.get(eventType), + listeners, + map; + + if (!eventTypeRegistration) { + // this eventType wasn't being observed at all + return; + } + + // the event type was observed; see if the target was registered + listeners = eventTypeRegistration.get(target); + if (!listeners) { + return; + } + + // the target was being observed for this eventType; see if the specified listener was registered + if (listeners === listener) { + eventTypeRegistration.set(target,null); + this._unregisterTargetForEventTypeIfNeeded(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners); + return; + } + else if (Array.isArray(listeners)) { + if (listeners.indexOf(listener) === -1) { + return; + } + if (this._currentDispatchedTargetListeners.has(listeners)) { + map = this._currentDispatchedTargetListeners.get(listeners); + if (!map) { + this._currentDispatchedTargetListeners.set(listeners,(map = new Map())); + } + map.set(listener,true); + + } else { + this.spliceOne(listeners,listeners.indexOf(listener)); + // Done unregistering the listener for the specified phase + // Now see if we need to remove any registrations as a result of that + this._unregisterTargetForEventTypeIfNeeded(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners); + + } + } + else { + //There's only one listener and it's no this one. + return; + } + + // console.log("EventManager.unregisteredEventListener", this.registeredEventListeners) + } + }, + _unregisterTargetForEventTypeIfNeeded: { + value: function(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners) { + if (!Array.isArray(listeners) || listeners.length === 0) { + var eventTypeRegistration = registeredEventListeners.get(eventType), + otherPhaseEventTypeRegistration = otherPhaseRegisteredEventListeners.get(eventType); + + eventTypeRegistration.delete(target); + + if (eventTypeRegistration.size === 0 && (!otherPhaseEventTypeRegistration || (otherPhaseEventTypeRegistration && otherPhaseEventTypeRegistration.size === 0))) { + // If no targets for this eventType; stop observing this event + //delete registeredEventListeners[eventType]; + this._stopObservingTarget_forEventType_(target, eventType); + } + } + } + }, + /** + * Determines the actual target to observe given a target and an eventType. + * This correctly decides whether to observe the element specified or + * to observe some other element to leverage event delegation. + * This should be consulted whenever starting or stopping the observation of + * a target for a given eventType. + * + * @function + * @param {Event} eventType + * @param {Event} target + * @returns null || target.screen ? target.document : target.ownerDocument + */ + actualDOMTargetForEventTypeOnTarget: { + value: function (eventType, target) { + + if (!target.nativeAddEventListener) { + return null; + } else { + + if (/*isDocument*/!!target.defaultView) { + return target; + } + + var entry = this.eventDefinitions[eventType], + bubbles; + + // For events we know we can safely delegate to handling at a higher level, listen on the document + // otherwise, be less surprising and listen on the specified target + + if (!entry) { + return target; + } + + // TODO allow eventTypes to describe a preferred delegation target window|document|none etc. + bubbles = (typeof entry.bubbles === "function") ? entry.bubbles(target) : entry.bubbles; + + if (bubbles) { + // TODO why on the document and not the window? + var shadowRoot; + return /* isWindow*/target.screen ? target.document : + (shadowRoot = this.shawdowRootFromNode(target)) ? + shadowRoot : target.ownerDocument; + } else { + return target; + } + } + } + }, + + shawdowRootFromNode: { + value: function isInShadow(node) { + if (window.ShadowRoot) { + while (node) { + if (node.toString() === "[object ShadowRoot]") { + return node; + } + node = node.parentNode; + } + } + } + }, + + /** + * @private + */ + _observedTarget_byEventType_: {value: null}, + + // Individual Event Registration + + _passiveEvents: { + enumerable: false, + value: [ + /* + // Disabled for flow + 'wheel', + 'mousewheel', + 'touchstart', + 'touchmove', + */ + 'scroll' + ] + }, + + isPassiveEventType: { + enumerable: false, + value: function (eventType) { + return this._passiveEvents.indexOf(eventType) !== -1; + } + }, + + /** + * @private + */ + _observeTarget_forEventType_: { + enumerable: false, + value: function (target, eventType, listenerOptions) { + + var listenerTarget; + + if ((listenerTarget = this.actualDOMTargetForEventTypeOnTarget(eventType, target)) && (!this._observedTarget_byEventType_[eventType] || !this._observedTarget_byEventType_[eventType].has(listenerTarget))) { + if (!this._observedTarget_byEventType_[eventType]) { + this._observedTarget_byEventType_[eventType] = new Map(); + } + this._observedTarget_byEventType_[eventType].set(listenerTarget,this); + + var eventDefinition = this.eventDefinitions[eventType], + eventOpts; + + if(!eventDefinition) { + console.debug("Event type "+eventType+" missed definition"); + } + + eventOpts = this.isPassiveEventType(eventType) + ? {passive: true} + : eventDefinition + ? eventDefinition.bubbles + : true; //by default + + + // eventOpts = { + // passive: this.isPassiveEventType(eventType), + // capture: true + // } + + listenerTarget.nativeAddEventListener((eventDefinition ? (eventDefinition.type || eventType) : eventType), this, eventOpts); + } + // console.log("started listening: ", eventType, listenerTarget) + } + }, + + /** + * @private + */ + _stopObservingTarget_forEventType_: { + enumerable: false, + value: function (target, eventType) { + + var listenerTarget = this.actualDOMTargetForEventTypeOnTarget(eventType, target), + registeredTargetForActivation = this._registeredTargetForActivation.get(eventType); + + /* + In registerTargetForActivation, we listen to event types that start an interaction cycle. If these were removed, it would create bugs as they wouldn't be dispatched by the event manager anymore. So we keep track of these in this._registeredTargetForActivation, and we check before removing the native event listener that this is not one of them. + + Some of these are instaled on window, so we add a check to see it's the documennt of that window. Ideally we wouldn't have to do that, but that requires thorough testing. + */ + + if (listenerTarget && (!registeredTargetForActivation || (registeredTargetForActivation && ((registeredTargetForActivation !== listenerTarget) && (registeredTargetForActivation.document !== listenerTarget)))) ) { + this._observedTarget_byEventType_[eventType].delete(listenerTarget); + listenerTarget.nativeRemoveEventListener(eventType, this, true); + } + // console.log("stopped listening: ", eventType, window) + } + }, + + /** + * @private + */ + _activationHandler: { + enumerable: true, + value: null + }, + + // Toggle listening for EventManager + + /** + * @private + */ + _listenToWindow: { + enumerable: false, + value: function (aWindow) { + + // We use our own function to handle activation events so it's not inadvertently + // removed as a listener when removing the last listener that may have also been observing + // the same eventType of an activation event + if (!this._activationHandler) { + var eventManager = this; + this._activationHandler = function _activationHandler(evt) { + var eventType = evt.type, + canBecomeActiveTarget = eventType !== "mouseenter" && eventType !== "pointerenter", + touchCount; + + // Prepare any components associated with elements that may receive this event + // They need to registered there listeners before the next step, which is to find the components that + // observing for this type of event + if (evt.changedTouches) { + touchCount = evt.changedTouches.length; + for (var i = 0; i < touchCount; i++) { + eventManager._prepareComponentsForActivation(evt.changedTouches[i].target, canBecomeActiveTarget); + } + } else { + eventManager._prepareComponentsForActivation(evt.target, canBecomeActiveTarget); + } + }; + } + + this.registerTargetForActivation(aWindow); + + if (this.application) { + + var applicationLevelEvents = this.registeredEventListenersOnTarget_(this.application), + eventType; + + for (eventType in applicationLevelEvents) { + if (applicationLevelEvents.hasOwnProperty(eventType)) { + this._observeTarget_forEventType_(aWindow, eventType); + } + } + } + } + }, + + _registeredTargetForActivation: { + value: new Map() + }, + + registerTargetForActivation: { + value: function (target) { + var _document = target instanceof Window ? target.document : target; + // The EventManager needs to handle "gateway/pointer/activation events" that we + // haven't let children listen for yet + // when the EM handles them eventually it will need to allow + // all components from the event target to the window to prepareForActivationEvents + // before finding event handlers that were registered for these events + if (window.PointerEvent) { + target.nativeAddEventListener("pointerdown", this._activationHandler, true); + this._registeredTargetForActivation.set("pointerdown", target); + + _document.nativeAddEventListener("pointerenter", this._activationHandler, true); + this._registeredTargetForActivation.set("pointerenter", _document); + + + } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) { + target.nativeAddEventListener("MSPointerDown", this._activationHandler, true); + this._registeredTargetForActivation.set("MSPointerDown", target); + + // IE10 has no support for pointerenter or pointerleave events. + _document.nativeAddEventListener("mouseenter", this._activationHandler, true); + this._registeredTargetForActivation.set("mouseenter", _document); + + } else { + target.nativeAddEventListener("touchstart", this._activationHandler, true); + this._registeredTargetForActivation.set("touchstart", target); + + target.nativeAddEventListener("mousedown", this._activationHandler, true); + this._registeredTargetForActivation.set("mousedown", target); + + // mouseenter events are not dispatched from window under Chrome and Safari. + _document.nativeAddEventListener("mouseenter", this._activationHandler, true); + this._registeredTargetForActivation.set("mouseenter", _document); + } + + target.nativeAddEventListener("focus", this._activationHandler, true); + this._registeredTargetForActivation.set("focus", target); + + // Clicking on a label element with a for attribute pointing to an + // input type radio or checkbox will check the input. In Chrome + // this will fire a focus event in the input element, this focus + // will call the _activationHandler, and that will allow a change + // event to fire in the input. In Safari, at least in the version at + // the time of writing this comment (15.5), the focus event is not + // fired and that was causing the event manager to fail to fire the + // expected change event. The following two lines fix the issue by + // directly listening to the change event in the window to call + // the _activationHandler: + + target.nativeAddEventListener("change", this._activationHandler, true); + this._registeredTargetForActivation.set("change", target); + + } + + }, + + /** + * @private + */ + _stopListeningToWindow: { + enumerable: false, + value: function (aWindow) { + + var applicationLevelEvents = this.registeredEventListenersOnTarget_(this.application), + windowLevelEvents = this.registeredEventListenersOnTarget_(aWindow), + eventType, + index; + + for (eventType in applicationLevelEvents) { + if (applicationLevelEvents.hasOwnProperty(eventType)) { + this._stopObservingTarget_forEventType_(aWindow, eventType); + } + } + + for (eventType in windowLevelEvents) { + if (windowLevelEvents.hasOwnProperty(eventType)) { + this._stopObservingTarget_forEventType_(aWindow, eventType); + } + } + + if ((index = this._listeningWindowOnTouchCancel.indexOf(aWindow)) > -1) { + this.spliceOne(this._listeningWindowOnTouchCancel,index); + // the listener on 'touchCancel' has already been removed by the previous step. + } + } + }, + + /** + * @function + */ + // _resetRegisteredEventListeners: { + // enumerable: false, + // value: function (registeredEventListeners) { + // var i, l, target, eventType, + // eventRegistration, + // self = this; + + // for (eventType in registeredEventListeners) { + // if (registeredEventListeners.hasOwnProperty(eventType)) { + // eventRegistration = registeredEventListeners.get(eventType); + + // if (eventRegistration && eventRegistration.length > 0) { + // for (i = 0, l = eventRegistration.length; i < l; i++) { + // target = eventRegistration[i]; + // self._stopObservingTarget_forEventType_(target, eventType); + // } + // } + // } + // } + // } + // }, + + reset: { + enumerable: false, + value: function () { + // this._resetRegisteredEventListeners(this._registeredCaptureEventListeners); + // this._resetRegisteredEventListeners(this._registeredBubbleEventListeners); + + self = this; + this._targetEventListeners.forEach(function(targetEntry/*value*/, target/*key*/, map) { + + targetEntry.forEach(function(targetEntryForEventType/*value*/, eventType/*key*/, map) { + self._stopObservingTarget_forEventType_(target, eventType); + }); + + }); + + + // TODO for each component claiming a pointer, force them to surrender the pointer? + this._claimedPointers = new Map(); + this._targetEventListeners = new Map(); + // this._registeredCaptureEventListeners = new Map(); + // this._registeredBubbleEventListeners = new Map(); + + } + }, + + + /** + * @function + */ + unload: { + enumerable: false, + value: function () { + this._stopListening(); + } + }, + _bubbleMethodNameByEventType_: { + value: new Map() + }, + _bubbleMethodNameByEventTypeIdentifier_: { + value: new Map() + }, + /** + * @function + */ + methodNameForBubblePhaseOfEventType: { + enumerable: false, + value: function methodNameForBubblePhaseOfEventType(eventType, identifier, capitalizedEventType, capitalizedIdentifier) { + var eventTypeBucket; + if (identifier) { + eventTypeBucket = this._bubbleMethodNameByEventTypeIdentifier_.get(eventType) || (this._bubbleMethodNameByEventTypeIdentifier_.set(eventType, new Map())).get(eventType); + return eventTypeBucket.get(identifier) || (eventTypeBucket.set(identifier, ("handle" + (capitalizedIdentifier || identifier.toCapitalized()) + (capitalizedEventType || eventType.toCapitalized())))).get(identifier); + } else { + return this._bubbleMethodNameByEventType_.get(eventType) || (this._bubbleMethodNameByEventType_.set(eventType, ("handle" + (capitalizedEventType || eventType.toCapitalized())))).get(eventType); + } + } + }, + + /** + * @private + */ + _captureMethodNameByEventType_: { + value: new Map() + }, + _catptureMethodNameByEventTypeIdentifier_: { + value: new Map() + }, + + methodNameForCapturePhaseOfEventType: { + enumerable: false, + value: function methodNameForCapturePhaseOfEventType(eventType, identifier, capitalizedEventType, capitalizedIdentifier) { + var eventTypeBucket; + if (identifier) { + eventTypeBucket = this._catptureMethodNameByEventTypeIdentifier_.get(eventType) || (this._catptureMethodNameByEventTypeIdentifier_.set(eventType,new Map())).get(eventType); + return eventTypeBucket.get(identifier) || (eventTypeBucket.set(identifier,("capture" + (capitalizedIdentifier || identifier.toCapitalized()) + (capitalizedEventType || eventType.toCapitalized())))).get(identifier); + } else { + return this._captureMethodNameByEventType_.get(eventType) || + (this._captureMethodNameByEventType_.set(eventType, ("capture" + (capitalizedEventType || eventType.toCapitalized())))).get(eventType); + } + } + }, + + // Claimed pointer information + + /** + * @private + */ + _claimedPointers: { + enumerable: false, + value: null + }, + + /** + * The component claiming the specified pointer component + * + * @function + * @param {string} pointer The pointer identifier in question + * @returns component + */ + componentClaimingPointer: { + value: function (pointer) { + return this._claimedPointers.get(pointer); + } + }, + + /** + * Whether or not the specified pointer identifier is claimed by the + * specified component. + * + * @function + * @param {string} pointer The pointer identifier in question + * @param {string} component The component to interrogate regarding + * ownership of the specified pointer + * @returns {boolean} + */ + isPointerClaimedByComponent: { + value: function (pointer, component) { + + if (!component) { + throw "Must specify a valid component to see if it claims the specified pointer, '" + component + "' is not valid."; + } + + return this._claimedPointers.get(pointer) === component; + } + }, + + /** + * Claims that a pointer, referred to by the specified pointer identifier, + * is claimed by the specified component. This does not give the component + * exclusive use of the pointer per se, but does indicate that the + * component is acting in a manner where it expects to be the only one + * performing major actions in response to this pointer. Other components + * should respect the claimant's desire to react to this pointer in order + * to prevent an entire hierarchy of components from reacting to a pointer + * in potentially conflicting ways. + * + * If the pointer is currently claimed by another component that component + * is asked to surrender the pointer, which is may or may not agree to do. + * + * @function + * @param {string} pointer The pointer identifier to claim + * @param {string} component The component that is claiming the specified + * pointer. + * @returns {boolean} - Whether or not the pointer was successfully claimed. + */ + claimPointer: { + value: function (pointer, component) { + + // if null, undefined, false: complain + if (!pointer && pointer !== 0) { + throw "Must specify a valid pointer to claim, '" + pointer + "' is not valid."; + } + + if (!component) { + throw "Must specify a valid component to claim a pointer, '" + component + "' is not valid."; + } + + var claimant = this._claimedPointers.get(pointer); + + if (claimant === component) { + // Already claimed this pointer ourselves + return true; + + } else if (!claimant) { + //Nobody has claimed it; go for it + this._claimedPointers.set(pointer,component); + return true; + + } else { + //Somebody else has claimed it; ask them to surrender + if (claimant.surrenderPointer(pointer, component)) { + this._claimedPointers.set(pointer,component); + return true; + } else { + return false; + } + } + + } + }, + + /** + * Forfeits the specified pointer identifier from the specified component. + * The specified component must be the current claimant. + * + * @function + * @param {string} pointer The pointer identifier in question + * @param {string} component The component that is trying to forfeit the + * specified pointer + */ + forfeitPointer: { + value: function (pointer, component) { + if (component === this._claimedPointers.get(pointer)) { + this._claimedPointers.delete(pointer); + } else { + throw "Not allowed to forfeit pointer '" + pointer + "' claimed by another component"; + } + + } + }, + + /** + * Forfeits all pointers from the specified component. + * + * @function + * @param {Component} component + */ + forfeitAllPointers: { + value: function (component) { + + var mapIter = this._claimedPointers.keys(), + pointerKey, + claimant; + + while ((pointerKey = mapIter.next().value)) { + claimant = this._claimedPointers.get(pointerKey); + if (component === claimant) { + // NOTE basically doing the work ofr freePointerFromComponent + this._claimedPointers.delete(pointerKey); + } + } + } + }, + + // Pointer Storage for calculating velocities + + /** + * @private + */ + _isStoringPointerEvents: { + enumerable: false, + value: false + }, + + /** + * @returns {boolean} + * @default false + */ + isStoringPointerEvents: { + enumerable: true, + get: function () { + return this._isStoringPointerEvents; + }, + set: function (value) { + if (value === true) { + if (!this._isStoringPointerEvents) { + this._isStoringPointerEvents = true; + + if ( + (typeof PointerEvent !== "undefined") || + (typeof MSPointerEvent !== "undefined" && typeof navigator !== "undefined" && navigator.msPointerEnabled) + ) { + Object.defineProperty(MutableEvent.prototype, "velocity", { + get: function () { + return defaultEventManager.pointerMotion(this.pointerId).velocity; + } + }); + } else if (typeof Touch !== "undefined") { + Object.defineProperty(Touch.prototype, "velocity", { + get: function () { + return defaultEventManager.pointerMotion(this.identifier).velocity; + } + }); + } + } + } else { + this._isStoringPointerEvents = false; + this._pointerStorage.memory = {}; + this._isMouseDragging = false; + } + } + }, + + /** + * @private + */ + _isStoringMouseEventsWhileDraggingOnly: { + enumerable: false, + value: true + }, + + /** + * @type {Function} + * @default {boolean} true + */ + isStoringMouseEventsWhileDraggingOnly: { + enumerable: true, + get: function () { + return this._isStoringMouseEventsWhileDraggingOnly; + }, + set: function (value) { + this._isStoringMouseEventsWhileDraggingOnly = (value === true) ? true : false; + } + }, + + /** + * @private + */ + _isMouseDragging: { + enumerable: false, + value: false + }, + + /** + * @private + */ + _pointerStorage: { + enumerable: false, + value: { + memory: {}, + velocity: {}, + add: function (identifier, data) { + if (!this.memory[identifier]) { + this.memory[identifier] = { + data: new Array(32), + size: 0, + pos: 0 + }; + } + this.memory[identifier].data[this.memory[identifier].pos] = data; + if (this.memory[identifier].size < this.memory[identifier].data.length) { + this.memory[identifier].size++; + } + this.memory[identifier].pos = (this.memory[identifier].pos + 1) % this.memory[identifier].data.length; + }, + remove: function (identifier) { + delete this.memory[identifier]; + }, + clear: function (identifier) { + if (this.memory[identifier]) { + this.memory[identifier].size = 0; + } + }, + getMemory: function (identifier) { + return this.memory[identifier]; + }, + isStored: function (identifier) { + return (this.memory[identifier] && (this.memory[identifier].size > 0)); + }, + storeEvent: function (mutableEvent) { + var isBrowserSupportPointerEvents = currentEnvironment.isBrowserSupportPointerEvents, + event = mutableEvent instanceof MutableEvent ? mutableEvent._event : mutableEvent, + pointerType = event + ? event.pointerType + : null; + if(!pointerType) return; + + if ((isBrowserSupportPointerEvents && + (pointerType === "mouse" || (window.MSPointerEvent && pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE))) || + (!isBrowserSupportPointerEvents && event instanceof MouseEvent)) { + + switch (mutableEvent.type) { + case "pointerdown": + case "MSPointerDown": + case "mousedown": + defaultEventManager._isMouseDragging = true; + /* falls through */ + // roll into mousemove. break omitted intentionally. + case "pointermove": + case "MSPointerMove": + case "mousemove": + if (defaultEventManager._isStoringMouseEventsWhileDraggingOnly) { + if (defaultEventManager._isMouseDragging) { + this._storeMouse(mutableEvent); + } + } else { + this._storeMouse(mutableEvent); + } + break; + + case "pointerup": + case "MSPointerUp": + case "mouseup": + this._storeMouse(mutableEvent); + break; + } + } else if ((isBrowserSupportPointerEvents && + (pointerType === "touch" || (window.MSPointerEvent && pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH))) || + (window.TouchEvent !== void 0 && !isBrowserSupportPointerEvents && event instanceof TouchEvent)) { + + switch (event.type) { + case "pointerdown": + case "MSPointerDown": + case "pointermove": + case "MSPointerMove": + case "pointerup": + case "MSPointerUp": + this._storeTouch(event.pointerId, event.clientX, event.clientY, event.timeStamp); + break; + + case "touchstart": + case "touchmove": + this._storeTouches(event.touches, event.timeStamp); + break; + + case "touchend": + this._storeTouches(event.changedTouches, event.timeStamp); + break; + } + } + }, + + removeEvent: function (mutableEvent) { + var isBrowserSupportPointerEvents = currentEnvironment.isBrowserSupportPointerEvents, + event = mutableEvent instanceof MutableEvent ? mutableEvent._event : mutableEvent; + + if ((isBrowserSupportPointerEvents && + (mutableEvent.pointerType === "mouse" || (window.MSPointerEvent && mutableEvent.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE))) || + (!isBrowserSupportPointerEvents && event instanceof MouseEvent)) { + + if (mutableEvent.type === "mouseup" || mutableEvent.type === "pointerup" || mutableEvent.type === "MSPointerUp") { + defaultEventManager._isMouseDragging = false; + + if (defaultEventManager._isStoringMouseEventsWhileDraggingOnly) { + this.clear("mouse"); + } + } + } else if ((isBrowserSupportPointerEvents && + (mutableEvent.pointerType === "touch" || (window.MSPointerEvent && mutableEvent.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH))) || + (window.TouchEvent !== void 0 && !isBrowserSupportPointerEvents && event instanceof TouchEvent)) { + + if (mutableEvent.type === "touchend" || mutableEvent.type === "pointerup" || mutableEvent.type === "MSPointerUp") { + if (mutableEvent.changedTouches) { + for (var i = 0, changedTouches = mutableEvent.changedTouches, iChangedTouch; (iChangedTouch = changedTouches[i]); i++) { + this.remove(iChangedTouch.identifier); + } + } else { + this.remove(mutableEvent.pointerId); + } + } + } + }, + + _storeMouse: function (event) { + this.add("mouse", { + clientX: event.clientX, + clientY: event.clientY, + timeStamp: event.timeStamp + }); + + Object.defineProperty(event, "velocity", { + get: function () { + return defaultEventManager.pointerMotion("mouse").velocity; + } + }); + }, + + _storeTouches: function (touches, timeStamp) { + var touch; + + for (var i = 0; (touch = touches[i]); i++) { + this._storeTouch(touch.identifier, touch.clientX, touch.clientY, timeStamp); + } + }, + + _storeTouch: function (identifier, clientX, clientY, timeStamp) { + this.add(identifier, { + clientX: clientX, + clientY: clientY, + timeStamp: timeStamp + }); + } + } + }, + + /** + @private + */ + _getPointerVelocityData: { + enumerable: false, + value: function (identifier) { + var i = 0, + memory, + memoryLength, + evt, + startTime, + iTime, + oldTime, oldX, oldY, squaredModule, + addData = true, + data = { + x: [], + y: [], + time: [] + }; + memory = defaultEventManager._pointerStorage.getMemory(identifier); + memoryLength = memory.data.length; + evt = memory.data[((memory.pos - 1) + memoryLength) % memoryLength]; + startTime = iTime = oldTime = evt.timeStamp; + oldX = evt.clientX; + oldY = evt.clientY; + while (addData && (iTime > startTime - 350) && (i < memory.size)) { + evt = memory.data[((memory.pos - i - 1) + memoryLength) % memoryLength]; + iTime = evt.timeStamp; + squaredModule = oldX * oldX + oldY * oldY; + if ((squaredModule > 2) && ((oldTime - iTime) <= 50)) { + data.x.push(evt.clientX); + data.y.push(evt.clientY); + data.time.push(iTime); + oldTime = iTime; + oldX = evt.clientX; + oldY = evt.clientY; + i++; + } else { + addData = false; + } + } + return data; + } + }, + + /** + @private + */ + _fitPointerCurve: { + enumerable: false, + value: function (bezier, data) { + var a, b, c, d, epsilon = 0.0001, + dl = data.length, e, t, v, t2, t3, i, + f0, c0, d0, b0, a0, s0, e0, + f1, c1, d1, b1, a1, s1, e1, + f2, c2, d2, b2, a2, s2, e2, + f3, c3, d3, b3, a3, s3, e3; + do { + f0 = c0 = d0 = b0 = a0 = s0 = f1 = c1 = d1 = b1 = a1 = s1 = f2 = c2 = d2 = b2 = a2 = s2 = f3 = c3 = d3 = b3 = a3 = s3 = 0; + for (i = 0; i < dl; i++) { + e = data[i]; + t = e.t; + t2 = t * t; + t3 = t2 * t; + v = e.v; + e0 = epsilon * (6 * (t2 - t) - t3 + 2); + e1 = epsilon * 6 * (t3 - 2 * t2 + t); + e2 = epsilon * 6 * (t2 - t3); + e3 = epsilon * 2 * t3; + s0 += e0 * e0; + s1 += e1 * e1; + s2 += e2 * e2; + s3 += e3 * e3; + f0 += v * e0; + f1 += v * e1; + f2 += v * e2; + f3 += v * e3; + d0 -= e0; + d1 -= e1; + d2 -= e2; + d3 -= e3; + c0 -= e0 * t; + c1 -= e1 * t; + c2 -= e2 * t; + c3 -= e3 * t; + b0 -= e0 * t2; + b1 -= e1 * t2; + b2 -= e2 * t2; + b3 -= e3 * t2; + a0 -= e0 * t3; + a1 -= e1 * t3; + a2 -= e2 * t3; + a3 -= e3 * t3; + } + epsilon *= 2; + } while (s0 === 0 || s1 === 0 || s2 === 0 || s3 === 0); + t = epsilon / s0; + f0 *= t; + c0 *= t * 3; + d0 *= t; + b0 *= t * 3; + a0 *= t; + t = epsilon / s1; + f1 *= t; + c1 *= t * 3; + d1 *= t; + b1 *= t * 3; + a1 *= t; + t = epsilon / s2; + f2 *= t; + c2 *= t * 3; + d2 *= t; + b2 *= t * 3; + a2 *= t; + t = epsilon / s3; + f3 *= t; + c3 *= t * 3; + d3 *= t; + b3 *= t * 3; + a3 *= t; + s0 = bezier[0]; + s1 = bezier[1]; + s2 = bezier[2]; + s3 = bezier[3]; + a = (s1 - s2) * 3 + s3 - s0; + b = s0 + s2 - 2 * s1; + c = s1 - s0; + d = s0; + for (i = 0; i < 20; i++) { + t = f0 + d * d0 + c * c0 + b * b0 + a * a0; + s0 += t; + d += t; + a -= t; + b += t; + c -= t; + t = f1 + d * d1 + c * c1 + b * b1 + a * a1; + s1 += t; + a += t * 3; + b -= t + t; + c += t; + t = f2 + d * d2 + c * c2 + b * b2 + a * a2; + s2 += t; + a -= t * 3; + b += t; + t = f3 + d * d3 + c * c3 + b * b3 + a * a3; + s3 += t; + a += t; + } + bezier[0] = s0; + bezier[1] = s1; + bezier[2] = s2; + bezier[3] = s3; + } + }, + + /** + @private + */ + _pointerBezierValue: { + enumerable: false, + value: function (t, bezier) { + var it = 1 - t; + return it * it * it * bezier[0] + 3 * it * it * t * bezier[1] + 3 * it * t * t * bezier[2] + t * t * t * bezier[3]; + } + }, + /** + @private + */ + _calculatePointerVelocity: { + enumerable: false, + value: function (time, position) { + var length = time.length, + timeMin = time[0], + timeMax = time[0], + timeInterval, + iMin = 0, i; + for (i = 1; i < length; i++) { + if (time[i] < timeMin) { + timeMin = time[i]; + iMin = i; + } + } + timeInterval = timeMax - timeMin; + if (timeInterval) { + if (length > 5) { + var s, e, bezier, data = []; + for (i = 0; i < length; i++) { + data[i] = { + v: position[i], + t: (time[i] - timeMin) / timeInterval + }; + } + s = data[iMin].v; + e = data[0].v; + bezier = [s, (s * 2 + e) / 3, (s + e * 2) / 3, e]; + this._fitPointerCurve(bezier, data); + return (this._pointerBezierValue(0.8, bezier) - this._pointerBezierValue(0.6, bezier)) * 5000 / timeInterval; + } else if (length > 1) { + return (position[0] - position[iMin]) * 1000 / timeInterval; + } else { + return 0; + } + } else { + return 0; + } + } + }, + + /** + @function + @param {attribute} identifier + */ + pointerMotion: { + value: function (identifier) { + if (defaultEventManager._pointerStorage.isStored(identifier)) { + return {velocity: (new _PointerVelocity()).initWithIdentifier(identifier)}; + } else { + return undefined; + } + } + }, + + monitorDOMModificationInEventHandling: {value: false}, + + domModificationEventHandler: { + value: Montage.specialize({ + handleEvent: { + value: function (event) { + throw "DOM Modified"; + } + }, + captureDOMSubtreeModified: { + value: function (event) { + throw "DOMSubtreeModified"; + } + }, + captureDOMAttrModified: { + value: function (event) { + throw "DOMAttrModified"; + } + }, + captureDOMCharacterDataModified: { + value: function (event) { + throw "DOMCharacterDataModified"; + } + } + }) + }, + + _trackingTouchStartList: { + value: null + }, + _trackingTouchEndList: { + value: null + }, + + _trackingTouchTimeoutIDs: { + value: null + }, + + _wouldTouchTriggerSimulatedEvent: { + value: function (event) { + + switch (event.type) { + case "touchstart": + case "touchend": + return true; + default: + return false; + } + + } + }, + + _couldEventBeSimulated: { + value: function (event) { + + switch (event.type) { + case "mousedown": + case "mouseup": + case "click": + return true; + default: + return false; + } + + } + }, + + _listeningWindowOnTouchCancel: { + value: [] + }, + + _findWindowFromEvent: { + value: function (event) { + var target = event.target, + aWindow; + + if (target) { + aWindow = target instanceof Window ? target : target.defaultView instanceof Window ? + target.defaultView : target.ownerDocument && target.ownerDocument.defaultView ? + target.ownerDocument.defaultView : null; + } + + return aWindow; + } + }, + + _isWindowListeningOnTouchCancel: { + value: function (aWindow) { + return this._registeredWindows.indexOf(aWindow) > -1 && this._listeningWindowOnTouchCancel.indexOf(aWindow) > -1; + } + }, + + _listenToTouchCancelIfNeeded: { + value: function (event) { + var aWindow = this._findWindowFromEvent(event); + + if (aWindow && !this._isWindowListeningOnTouchCancel(aWindow)) { + var self = this; + + aWindow.addEventListener("touchcancel", function touchcancelListener(event) { + var changedTouches = event.changedTouches, + touchesStartList = self._trackingTouchStartList, + identifier; + + for (var i = 0, length = changedTouches.length; i < length; i++) { + identifier = changedTouches[i].identifier; + + if (touchesStartList.has(identifier)) { + touchesStartList.delete(identifier); + } + } + }, true); + + this._listeningWindowOnTouchCancel.push(aWindow); + } + } + }, + + _blocksEmulatedEvents: { + value: true + }, + blocksEmulatedEvents: { + get: function() { return this._blocksEmulatedEvents; }, + set: function(value) { + if (value !== this._blocksEmulatedEvents) { + this._blocksEmulatedEvents = value; + this._evaluateShouldDispatchEventCondition(); + } + } + }, + _shouldDispatchEventCondition: { + value: undefined + }, + + _evaluateShouldDispatchEventCondition: { + value: function() { + this._shouldDispatchEventCondition = (this.blocksEmulatedEvents && !window.PointerEvent && + !(window.MSPointerEvent && window.navigator.msPointerEnabled)); + } + }, + + + /** + * @function + * @param {Event} event + * @description Decides if an event can be dispatched by the EventManager within a montage app. + * Filter emulated mouse events (mousedown, mouseup, click) from touch events. + * + * @private + */ + _shouldDispatchEvent: { + value: function (event) { + if (this._shouldDispatchEventCondition) { + /** + * Under IOS < 10.3.1, emulated mouse events have a timestamp set to 0. (Just WKWebView not UIWebView) + * starting with 10.3.1, timeStamp is non null. However, emulated events don't have the property movementX/Y + * that desktop Safari has, so we're adding logic to leverage that. + * Plus, this property can't be used for Firefox. + * Firefox has an open bug since 2004: the property timeStamp is not populated correctly. + * -> https://bugzilla.mozilla.org/show_bug.cgi?id=238041 + */ + if (this.environment.isIOSDevice && this.environment.isWKWebView) { + if (event.timeStamp === 0) { + return false; + } + if (this._couldEventBeSimulated(event) && !event.hasOwnProperty("movementX")) { + return false; + } + } + + /** + * No needs do dispatch mouse events on android devices. + */ + if (this.environment.isAndroidDevice && this._couldEventBeSimulated(event)) { + return false; + } + + // Checks if the event may trigger simulated events. + if (this._wouldTouchTriggerSimulatedEvent(event)) { + var changedTouches = event.changedTouches; + + // Needs to clean the tracking touches "start" when a touch event is canceled. + this._listenToTouchCancelIfNeeded(event); + + for (var i = 0, length = changedTouches.length; i < length; i++) { + this._trackTouch(event, changedTouches[i]); + } + + } else if (this._couldEventBeSimulated(event)) { // Determines if mouse events are simulated. + return !this._isEmulatedEvent(event); + } // else -> Dispatches all the others. + } + + return true; + } + }, + + _trackTouch: { + value: function (touchEvent, touch) { + var timeoutIDs = this._trackingTouchTimeoutIDs, + trackingTouchStartList = this._trackingTouchStartList, + trackingTouchEndList = this._trackingTouchEndList, + touchIdentifier = touch.identifier; + + touch.timeStamp = touchEvent.timeStamp; + + if (touchEvent.type === "touchstart") { + /** + * Touch identifiers are not unique for Firefox or Chrome (they are re-used). + * So, we need to clear the timeout that was supposed to clean this tracking touch. + */ + var timeoutID = timeoutIDs.get(touchIdentifier); + + if (timeoutID) { + clearTimeout(timeoutID); + timeoutIDs.delete(touchIdentifier); + } + + trackingTouchStartList.set(touchIdentifier,touch); + + } else { // touchend + timeoutIDs.set(touchIdentifier, setTimeout(function () { + trackingTouchEndList.delete(touchIdentifier); + delete timeoutIDs[touchIdentifier]; + }, 400)); + /** + * 400ms -> need a higher timeout than the click delay for UIWebViews. + * Probably related to the fact Apple pauses JavaScript execution during scrolls on UIWebViews. + * http://developer.telerik.com/featured/scroll-event-change-ios-8-big-deal/ + */ + + trackingTouchStartList.delete(touchIdentifier); + trackingTouchEndList.set(touchIdentifier,touch); + } + // console.groupTimeEnd("_trackTouch"); + + } + }, + + /** + * @function + * @param {Event} event + * @description Decides if an event is an emualted mouse event. + * Checks if a target has already been "activated" by a touch. + * + * @private + */ + _isEmulatedEvent: { + value: function (event) { + /** + * Can't use the position, indeed the emulated mouse events are not at the same position + * than the touch events than triggered them. Doesn't work with a radius as well. + * (using a radius can make it fail with wide fast movements -> FF) + * + * Needs to check both maps for devices with multiples pointers + * (touchstart + mousedown -> mouseup -> click) or (touchstart + delay ≈ 600ms -> mousedown) + */ + var response = this._findEmulatedEventIdentifierWithEventAndTrackingTouchList(event, this._trackingTouchStartList) > -1; + + if (!response) { + var trackingTouchList = this._trackingTouchEndList, + identifier = this._findEmulatedEventIdentifierWithEventAndTrackingTouchList(event, trackingTouchList); + + // Faster "awake", can be useful for devices with multiple pointers. (simultaneous click/touch) + if ((response = identifier > -1) && event.type === "click") { + var timeoutIDs = this._trackingTouchTimeoutIDs, + timeoutID = timeoutIDs.get(identifier); + + if (timeoutID) { + clearTimeout(timeoutID); + + trackingTouchList.delete(identifier); + timeoutIDs.delete(identifier); + } + } + } + + return response; + } + }, + + _emulatedEventTimestampThreshold: { + value: 20 //ms + }, + + _emulatedEventRadiusThreshold: { + value: 20 //px + }, + + /** + * @function + * @private + * + */ + _findEmulatedEventIdentifierWithEventAndTrackingTouchList: { + value: function (mouseEvent, trackingTouchList) { + var mouseTarget = mouseEvent.target, + identifier = -1, + key, + touch, + mapIter; + + mapIter = trackingTouchList.keys(); + while ((key = mapIter.next().value)) { + touch = trackingTouchList.get(key); + + if (touch.target === mouseTarget || + this._couldEmulatedEventHaveWrongTarget( + touch, + mouseEvent, + this._emulatedEventRadiusThreshold, + this._emulatedEventTimestampThreshold + )) { + + identifier = key; + break; + } + } + return identifier; + } + }, + + + /** + * @function + * @private + * @description Check if a mouse event can not be a simulated event even if the target is different. + * Indeed, Touch Events and simulated Mouse Events can have a different target and not the same position on Chrome. + * + */ + _couldEmulatedEventHaveWrongTarget: { + value: function (touch, mouseEvent, radiusThreshold, timestampThreshold) { + + if (/*dTimestamp*/(mouseEvent.timeStamp - touch.timeStamp) <= timestampThreshold) { + var dX = touch.clientX - mouseEvent.clientX, + dY = touch.clientY - mouseEvent.clientY; + + return dX * dX + dY * dY <= radiusThreshold * radiusThreshold; + } + + return false; + } + }, + + + // Event Handling + /** + * @property {Array} + * @description the pointer to the current candidate event listeners. + * + * @private + */ + _currentDispatchedTargetListeners: { + value:null + }, + /** + * @function + * @description the pointer to the current candidate event listeners. + * + * @private + */ + // _processCurrentDispatchedTargetListenersToRemove: { + // value: function(target, eventType, useCapture, listeners) { + + // var registeredEventListeners, + // otherPhaseRegisteredEventListeners, + // currentDispatchedTargetListenersToRemove = this._currentDispatchedTargetListeners.get(listeners); + + // if (currentDispatchedTargetListenersToRemove && currentDispatchedTargetListenersToRemove.size > 0) { + // listeners.removeObjects(currentDispatchedTargetListenersToRemove); + // registeredEventListeners = useCapture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners; + // otherPhaseRegisteredEventListeners = useCapture ? this._registeredBubbleEventListeners : this._registeredCaptureEventListeners; + // this._unregisterTargetForEventTypeIfNeeded(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners); + // } + // } + // }, + _integerAscendingSortFunction: { + value: function(a, b) { + return a - b; + } + }, + _processCurrentDispatchedTargetListenersToRemove: { + value: function(target, eventType, phase, listeners) { + + var registeredEventListeners, + otherPhaseRegisteredEventListeners, + //currentDispatchedTargetListenersToRemove is an array that contains the indexes of the listenerEntries that needs to be removed post phase distribution + currentDispatchedTargetListenersToRemove = this._currentDispatchedTargetListeners.get(listeners); + + if (currentDispatchedTargetListenersToRemove && currentDispatchedTargetListenersToRemove.length > 0) { + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null, + targetEntryForEventTypeListeners = targetEntryForEventType + ? targetEntryForEventType.get(phase) + : null; + + //Remove what needs to be + currentDispatchedTargetListenersToRemove.sort(this._integerAscendingSortFunction); + for(var i = 0, countI = currentDispatchedTargetListenersToRemove.length, index; i < countI; i++) { + index = currentDispatchedTargetListenersToRemove[i] - i; + targetEntryForEventTypeListeners.splice(index, 1); + } + + this._stopObservingTargeForEventTypeIfNeeded(target, eventType, targetEntryForEventType); + } + } + }, + + _invokeTargetListenersForEventPhase: { + value: function(iTarget, mutableEvent, phase, _eventType, promise) { + if(promise && promise.then) { + return promise.then(() => { + if(!mutableEvent.immediatePropagationStopped) { + return this.__invokeTargetListenersForEventPhase(iTarget, mutableEvent, phase, _eventType); + } + }); + } else { + return this.__invokeTargetListenersForEventPhase(iTarget, mutableEvent, phase, _eventType); + } + } + }, + + + __invokeTargetListenersForEventPhase: { + value: function __invokeTargetListenersForEventPhase(iTarget, mutableEvent, phase, _eventType) { + var eventType = (_eventType || mutableEvent.type), + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, phase); + + if (listenerEntries) { + + // currentTargetIdentifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); + if (Array.isArray(listenerEntries)) { + var j=0, + _currentDispatchedTargetListeners = this._currentDispatchedTargetListeners, + nextEntry, + previousPromise, + promise, + // promises, + mutableEventPhase = mutableEvent.eventPhase, + self = this; + + _currentDispatchedTargetListeners.set(listenerEntries,null); + while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { + if(promise) { + if(promise.then) { + previousPromise = promise; + } + } + promise = this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, mutableEventPhase, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/, previousPromise); + + // if(previousPromise && promise) { + // if(!promises) { + // promises = [previousPromise, promise]; + // } else { + // promises.push(promise); + // } + // } + } + + if(/*promises || */promise && promise.then) { + /* + We now chain each listener promise, so we can enforce an stopImmedidatePopagation + + mutableEvent.immediatePropagationStopped + + So no need to do a promise.all anymore + */ + // promise = (promises && promises.length) + // ? Promise.all(promises) + // : promise; + + return promise.finally(function() { + self._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, phase, listenerEntries); + _currentDispatchedTargetListeners.delete(listenerEntries); + }); + + } else { + this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, phase, listenerEntries); + _currentDispatchedTargetListeners.delete(listenerEntries); + return; + } + } + else { + /* + There's only one listener, so no previous promise to pass + */ + return this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, mutableEvent.eventPhase, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/, /*promise*/undefined); + } + } + } + }, + + /** + @function + @param {Event} event The handled event. + + - If handleEvent() know the dispatch is sync, we can re-use the same handleEvent() and keep track of the stack + - Otherwise, coukd we do: + + function EventManager_handleEvent(event) { + - Let currentHandleEvent = this.handleEvent, + - nestedHandleEvent = (nestedEvent) => { + + let referrerEvent = this._eventByNestedHandleEvent.get(nestedHandleEvent); + nestedEvent.referrer = referrerEvent; + let promise = EventManager_handleEvent.call(this, event); + if(promise) { + promise.then(() => { + this._eventByNestedHandleEvent.delete(nestedHandleEvent); + }) + } + ... + } + this._eventByNestedHandleEvent.set(nestedHandleEvent, event); + + + + + */ + handleEvent: { + enumerable: false, + value: function EventManager_handleEvent(event) { + // if(event.type === "pointerdown" || event.type === "pointerup" || event.type.indexOf("press") !== -1) { + // console.log("handleEvent "+event.type,event.target); + // } + //console.log("----> handleEvent "+event.type); + var i, + iTarget, + eventPath, + eventType = event.type, + eventDefinition = this.eventDefinitions[eventType], + eventBubbles = event.bubbles, + mutableEvent, + mutableEventTarget, + + //New stuff + CAPTURING_PHASE = Event_CAPTURING_PHASE, + BUBBLING_PHASE = Event_BUBBLING_PHASE, + targetEntry, targetEntryForEventType, + promise; + + if(this.isBrowser) { + if( + (MontageElement && event.target instanceof MontageElement) || + (event instanceof UIEvent && !this._shouldDispatchEvent(event)) + ) { + return void 0; + } + + // performance.mark('event-manager:handleEvent:start'); + + if (this.monitorDOMModificationInEventHandling) { + document.body.addEventListener("DOMSubtreeModified", this.domModificationEventHandler, true); + document.body.addEventListener("DOMAttrModified", this.domModificationEventHandler, true); + document.body.addEventListener("DOMCharacterDataModified", this.domModificationEventHandler, true); + } + + if ("DOMContentLoaded" === eventType) { + var loadedWindow = event.target.defaultView; + if (loadedWindow && this._windowsAwaitingFinalRegistration.has(loadedWindow)) { + this._finalizeWindowRegistration(loadedWindow); + // Stop listening for DOMContentLoaded on this target + // Otherwise the eventManager's handleEvent will be called + // again from within here when the eventManager is found + // to be a listener for this event when we find the listeners + event.target.removeEventListener("DOMContentLoaded", this, true); + } + } + } + + + if (typeof event.propagationStopped !== "boolean") { + mutableEvent = MutableEvent.fromEvent(event); + } else { + mutableEvent = event; + } + + if(eventDefinition && eventDefinition.type) { + mutableEvent.type = eventType = eventDefinition.type; + } + + mutableEventTarget = mutableEvent.target; + eventPath = this.composedPathForEvent(mutableEvent); + /* + This isn't used anywhere and could override ._composedPath set from the outside + The only use in SynchronizationService - readOperation.composedPath().delete(this.destinationDataService); + should we do: + if(!mutableEvent._composedPath) { + mutableEvent._composedPath = eventPath; + } + + instead? + */ + mutableEvent._composedPath = eventPath; + + + // Let the delegate handle the event first + if (this.delegate && typeof this.delegate.willDistributeEvent === "function") { + this.delegate.willDistributeEvent(mutableEvent); + } + + if (this._isStoringPointerEvents) { + this._pointerStorage.storeEvent(mutableEvent); + } + + + targetEntry = this._targetEventListeners.get(mutableEventTarget); + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null; + + // Capture Phase Distribution + mutableEvent.eventPhase = CAPTURING_PHASE; + /* + The event path we generate is from bottom to top, capture needs to traverse this backwards + As eventPath now includes the target, we need to stop at index 1 in capture, and start from in bubble + */ + i = eventPath.length; + let countI = i; + while (i>1 && !mutableEvent.immediatePropagationStopped && !mutableEvent.propagationStopped && (iTarget = eventPath[--i])) { + if(eventPath.length !== countI || mutableEvent.currentTarget === iTarget) { + //eventPath was modified + while((iTarget = eventPath[--i]) === mutableEvent.currentTarget) {}; + } + mutableEvent.currentTarget = iTarget; + + promise = this._invokeTargetListenersForEventPhase(iTarget, mutableEvent, CAPTURING_PHASE, eventType, promise); + } + + // At Target Distribution + if (!mutableEvent.immediatePropagationStopped && !mutableEvent.propagationStopped) { + mutableEvent.eventPhase = Event_AT_TARGET; + mutableEvent.currentTarget = iTarget = mutableEventTarget; + //Capture + promise = this._invokeTargetListenersForEventPhase(iTarget, mutableEvent, CAPTURING_PHASE, eventType, promise); + + //Bubble + promise = this._invokeTargetListenersForEventPhase(iTarget, mutableEvent, BUBBLING_PHASE, eventType, promise); + } + + /* + Bubble Phase Distribution + As eventPath now includes the target, we need to stop at index 1 in capture, and start from in bubble + */ + if(eventBubbles) { + mutableEvent.eventPhase = BUBBLING_PHASE; + for (i = 1; !mutableEvent.immediatePropagationStopped && !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i++) { + mutableEvent.currentTarget = iTarget; + + promise = this._invokeTargetListenersForEventPhase(iTarget, mutableEvent, BUBBLING_PHASE, eventType, promise); + + } + } + + + if(promise) { + var self = this; + mutableEvent.propagationPromise = promise.then(function() { + self._finalizeHandleEvent(mutableEvent, event); + }); + } else { + this._finalizeHandleEvent(mutableEvent, event); + } + + //console.profileEnd("handleEvent "+event.type); + //console.groupTimeEnd("handleEvent"); + + // performance.mark('event-manager:handleEvent:end'); + // performance.measure('handleEvent', 'event-manager:handleEvent:start', 'event-manager:handleEvent:end'); + } + }, + + _finalizeHandleEvent: { + value: function(mutableEvent, event) { + mutableEvent.eventPhase = Event_NONE; + mutableEvent.currentTarget = null; + + if(this.isBrowser) { + if (this._isStoringPointerEvents) { + this._pointerStorage.removeEvent(event); + } + + if (this.monitorDOMModificationInEventHandling) { + document.body.removeEventListener("DOMSubtreeModified", this.domModificationEventHandler, true); + document.body.removeEventListener("DOMAttrModified", this.domModificationEventHandler, true); + document.body.removeEventListener("DOMCharacterDataModified", this.domModificationEventHandler, true); + } + } + + + } + }, + + /** + * @private + */ + // _invokeTargetListenerForEvent: { + // value: function _invokeTargetListenerForEvent(iTarget, jListener, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { + // // var functionType = "function"; + // // if (typeof jListener === functionType) { + // // jListener.call(iTarget, mutableEvent); + // // } + // // else if (identifierSpecificPhaseMethodName && typeof jListener[identifierSpecificPhaseMethodName] === functionType) { + // // jListener[identifierSpecificPhaseMethodName](mutableEvent); + // // } + // // else if (typeof jListener[phaseMethodName] === functionType) { + // // jListener[phaseMethodName](mutableEvent); + // // } + // // else if (typeof jListener.handleEvent === functionType) { + // // jListener.handleEvent(mutableEvent); + // // } + // (currentTargetIdentifierSpecificPhaseMethodName && typeof jListener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + // ? jListener[currentTargetIdentifierSpecificPhaseMethodName](mutableEvent) + // : (targetIdentifierSpecificPhaseMethodName && typeof jListener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + // ? jListener[targetIdentifierSpecificPhaseMethodName](mutableEvent) + // : (typeof jListener[phaseMethodName] === this._functionType) + // ? jListener[phaseMethodName](mutableEvent) + // : (typeof jListener.handleEvent === this._functionType) + // ? jListener.handleEvent(mutableEvent) + // : (typeof jListener === this._functionType) + // ? jListener.call(iTarget, mutableEvent) + // : void 0; + + // } + // }, + + _invokeTargetListenerEntryForEvent: { + value: function _invokeTargetListenerEntryForEvent(iTarget, listenerEntry, mutableEvent, phase, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName, promise) { + if(promise && promise.then) { + return promise.then(() => { + if(!mutableEvent.immediatePropagationStopped) { + return this.__invokeTargetListenerEntryForEvent(iTarget, listenerEntry, mutableEvent, phase, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName); + } else { + throw new Error("immediatePropagationStopped"); + } + }); + } else { + return this.__invokeTargetListenerEntryForEvent(iTarget, listenerEntry, mutableEvent, phase, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName); + } + } + }, + + + /** + * @method + * @param {String} type The type of an event. + * @param {Object} listener The listener for the event described. + * @param {Object} target The target of the event. + * @param {Boolean} capture true if asking for a callback in the capture phase, false for bubble. + + * @description Returns the function to invoke on listener when an event happens. This takes into account our logic on routing event callbacks + * based on event type, phase and target's identifier. + * + */ + + callbackForEventTypeListenerOnTargetInPhase: { + value: function callbackForEventTypeListenerOnTargetInPhase(type, listener, target, capture, currentTarget) { + var callback, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName; + + callback = ((currentTargetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(capture, type, currentTarget.identifier)) && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === "function") + ? currentTargetIdentifierSpecificPhaseMethodName + : ((targetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(capture,type,target.identifier)) && typeof listener[targetIdentifierSpecificPhaseMethodName] === "function") + ? targetIdentifierSpecificPhaseMethodName + : (typeof listener[(phaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(capture,type, null))] === "function") + ? phaseMethodName + : (typeof listener.handleEvent === "function") + ? "handleEvent" + : typeof listener === "function" + ? listener + : void 0; + + if(typeof callback === "string") { + callback = listener[callback]; + } + + return callback; + } + }, + + /** + * @private + */ + __invokeTargetListenerEntryForEvent: { + value: function __invokeTargetListenerEntryForEvent(iTarget, listenerEntry, mutableEvent, phase, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { + var listener = listenerEntry.listener, + callback, + // callbacks = listenerEntry.callbacks, + result; + + + //TEST, shutting currentTargetIdentifierSpecificPhaseMethodName down: + //currentTargetIdentifierSpecificPhaseMethodName = null; + + // if(typeof listener === this._functionType) { + // result = listener.call(iTarget, mutableEvent); + // } else { + + + /* + Caching the callback needs the same fallback strategy as finding it, worth it? + */ + + //if(!(callback = (callbacks && callbacks.get(iTarget)))) { + + // callback = (currentTargetIdentifierSpecificPhaseMethodName && typeof (callback = listener[currentTargetIdentifierSpecificPhaseMethodName]) === this._functionType) + // ? callback + // : (targetIdentifierSpecificPhaseMethodName && typeof (callback = listener[targetIdentifierSpecificPhaseMethodName]) === this._functionType) + // ? callback + // : (typeof (callback = listener[phaseMethodName]) === this._functionType) + // ? callback + // : (typeof (callback = listener.handleEvent) === this._functionType) + // ? callback + // : void 0; + + + // callback = ((currentTargetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture, mutableEvent.type, iTarget.identifier)) && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + // ? currentTargetIdentifierSpecificPhaseMethodName + // : ((targetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type,mutableEvent.target.identifier)) && typeof listener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + // ? targetIdentifierSpecificPhaseMethodName + // : (typeof listener[(phaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type, null))] === this._functionType) + // ? phaseMethodName + // : (typeof listener.handleEvent === this._functionType) + // ? "handleEvent" + // : typeof listener === this._functionType + // ? listener + // : void 0; + + // if(typeof callback === "string") { + // callback = listener[callback]; + // } + + callback = this.callbackForEventTypeListenerOnTargetInPhase(mutableEvent.type, listener, mutableEvent.target, listenerEntry.capture, iTarget); + + // if(!listenerEntry.once) { + // if(!callbacks) { + // listenerEntry.callbacks = callbacks = new Map(); + // } + // callbacks.set(iTarget, callback); + // } + //} + + if(callback) { + + mutableEvent.eventPhase = phase; + mutableEvent.currentTarget = iTarget; + + + //callback.call(listener, mutableEvent); + result = typeof callback !== "function" + ? listener[callback](mutableEvent) + : (callback === listener) + ? callback.call(iTarget, mutableEvent) + : callback.call(listener, mutableEvent); + + } + //} + + if(listenerEntry.once) { + this.unregisterTargetEventListener(iTarget, mutableEvent.type, listener, listenerEntry); + } + + return result; + + } + }, + + _currentTargetIdentifierSpecificPhaseMethodName: { + value: function(capture, eventType, targetIdentifier) { + return capture + ? this.methodNameForCapturePhaseOfEventType(eventType, targetIdentifier) + : this.methodNameForBubblePhaseOfEventType(eventType, targetIdentifier); + } + }, + + + /** + * Ensure that any components associated with DOM elements in the hierarchy between the + * original activationEvent target and the window are preparedForActionEvents + * + * Benoit todo: Make this more generic and configurable to be applicable to different trees. + * + * @function + * @private + */ + _prepareComponentsForActivation: { + value: function (eventTarget, canBecomeActiveTarget) { + var target = eventTarget, + previousTarget, + targetView = target && target.defaultView ? target.defaultView : window, + targetDocument = targetView.document ? targetView.document : document, + associatedComponent, + lookedForActiveTarget = false, + activeTarget = null; + + do { + if (target) { + associatedComponent = this.eventHandlerForElement(target); + if (associatedComponent) { + + // Once we've found a component starting point, + // find the closest Target that accepts focus + if (!lookedForActiveTarget) { + lookedForActiveTarget = true; + activeTarget = this._findActiveTarget(associatedComponent); + } + + if (!associatedComponent.preparedForActivationEvents) { + associatedComponent._prepareForActivationEvents(); + } + } + } + + previousTarget = target; + + // We only need to go up to the window, and even that's debateable as the activationEvent system really + // only pertains to components, which are only ever associated with elements. The root element being the + // exception which is associated with the document. + switch (target) { + case targetView: + target = null; + break; + case targetDocument: + target = targetView; + break; + case targetDocument.documentElement: + target = targetDocument; + break; + default: + target = target.parentNode; + break; + } + + } while (target && previousTarget !== target); + + if (canBecomeActiveTarget) { + this.activeTarget = activeTarget; + } + } + }, + _findActiveTargetMap : { + value: undefined + }, + /** + * @private + */ + _findActiveTarget: { + value: function (target) { + + var foundTarget = null, + checkedTargetMap = this._findActiveTargetMap; + + checkedTargetMap.clear(); + //TODO report if a cycle is detected? + while (!foundTarget && target && !(checkedTargetMap.has(target))) { + + //TODO complain if a non-Target-alike is considered + + checkedTargetMap.set(target,true); + + if (target.acceptsActiveTarget) { + foundTarget = target; + } else { + target = target.nextTarget; + } + } + + return foundTarget; + } + }, + + /** + * Introduced to give the ability to the context creating and dispatching an event to also + * + * Benoit todo: Make this more generic and configurable to be applicable to different trees. + * + * @function + * @private + */ + composedPathForEvent: { + enumerable: false, + value: function (event) { + /* + if event is a MutableEvent, which it should be, + because currently MutableEvent doesn't wrap composedPath() to return : + return this._composedPath || this._event.composedPath() + + for a DOM event, + + event.composedPath() and this.eventPathForTarget(event.target) returns the same thing with one exception: + + the array returned by our this.eventPathForTarget(event.target) contains this.application as the last object + where the DOM obviously doesn't + + Now that a native composedPath() exists it should be much faster to use it than re-compute the same thing, + But we have to change how we handle application. + + We should be able to handler it as a one-off before the loop for capture phase + and as a one-off after the loop for bubble phase. + + To be tested but that should work + + Also to do fix: + + this._eventPathForTarget() returns an array that ends with [..., Application, ApplicationDelegate] + + */ + return event.composedPath() || this.eventPathForTarget(event.target); + } + }, + + _eventPathForTargetMap : { + value: undefined + }, + + eventPathForTarget: { + enumerable: false, + value: function (target) { + + if (this.isBrowser && (this.isElement(target) || target instanceof Document || target === window)) { + return this._eventPathForDomTarget(target); + } else { + return this._eventPathForTarget(target); + } + } + }, + + /** + * Build the event target chain for the the specified Target + * @private + */ + _eventPathForTarget: { + enumerable: false, + value: function (target) { + + if (!target) { + return this._emptyComposedPath; + } else if(target.composedPath) { + /* + If target has a commposedPath, it's likely cached. + So in case it's mutated by a listner, we're making a copy + to guard against that. + */ + return Array.from(target.composedPath); + } else { + + var targetCandidate = target, + application = this.application, + eventPath = [], + discoveredTargets = this._eventPathForTargetMap; + + discoveredTargets.clear(); + + // Consider the target "discovered" for less specialized detection of cycles + // discoveredTargets.set(target,true); + + do { + if (!discoveredTargets.has(targetCandidate)) { + eventPath.push(targetCandidate); + discoveredTargets.set(targetCandidate,true); + } + + targetCandidate = targetCandidate.nextTarget; + + if (!targetCandidate || discoveredTargets.has(targetCandidate)) { + targetCandidate = application; + } + + if (targetCandidate && discoveredTargets.has(targetCandidate)) { + targetCandidate = null; + } + } + while (targetCandidate); + + return eventPath; + } + + } + }, + + _emptyComposedPath: { + value: Object.freeze([]) + }, + /** + * Build the event target chain for the the specified DOM target + * @private + */ + _eventPathForDomTarget: { + enumerable: false, + value: function (target) { + + if (!target) { + return this._emptyComposedPath; + } + + var targetCandidate = target, + targetView = targetCandidate && targetCandidate.defaultView ? targetCandidate.defaultView : window, + targetDocument = targetView.document ? targetView.document : document, + targetApplication = this.application, + previousBubblingTarget, + eventPath = []; + + do { + // Include the target itself as the root of the event's compsoedPath + eventPath.push(targetCandidate); + + previousBubblingTarget = targetCandidate; + // use the structural DOM hierarchy until we run out of that and need + // to give listeners on document, window, and application a chance to respond + switch (targetCandidate) { + case targetApplication: + targetCandidate = targetCandidate.parentApplication; + if (targetCandidate) { + targetApplication = targetCandidate; + } + break; + case targetView: + targetCandidate = targetApplication; + break; + case targetDocument: + targetCandidate = targetView; + break; + case targetDocument.documentElement: + targetCandidate = targetDocument; + break; + default: + targetCandidate = targetCandidate.parentNode; + + // Run out of hierarchy candidates? go up to the application + if (!targetCandidate) { + targetCandidate = targetApplication; + } + + break; + } + } + while (targetCandidate && previousBubblingTarget !== targetCandidate); + + return eventPath; + } + }, + + /** + * @private + */ + _elementEventHandlerByElement: { + enumerable: false, + value: null + }, + + /** + @function + @param {Event} anElementEventHandler + @param {Element} anElement + */ + registerEventHandlerForElement: { + enumerable: false, + value: function (anElementEventHandler, anElement) { + // console.log("registerEventHandlerForElement",anElementEventHandler,anElementEventHandler,anElement) + var oldEventHandler = this.eventHandlerForElement(anElement); + // unreference unused event handlers + if (oldEventHandler) { + if(oldEventHandler !== anElementEventHandler) { + this.unregisterEventHandlerForElement(anElement); + this._elementEventHandlerByElement.set(anElement,anElementEventHandler); + } + } else { + this._elementEventHandlerByElement.set(anElement,anElementEventHandler); + } + } + }, + + /** + @function + @param {Element} anElement + */ + unregisterEventHandlerForElement: { + enumerable: false, + value: function (anElement) { + this._elementEventHandlerByElement.delete(anElement); + } + }, + /** + @function + @param {Element} anElement + @returns this._elementEventHandlerByElement.get(anElement) + */ + eventHandlerForElement: { + enumerable: false, + value: function (anElement) { + return this._elementEventHandlerByElement.get(anElement); + } + }, + + /** + * @private + */ + _activeTarget: { + value: null + }, + + /** + * The logical component that has focus within the application + * + * This can be used as the proximal target for dispatching in + * situations where it logically makes sense that and event, while + * created by some other component, should appear to originate from + * where the user is currently focused. + * + * This is particularly useful for things such as keyboard shortcuts or + * menuAction events. + * + * Prior to setting the activeTarget manually the desired target should + * be checked to see if it `acceptsActiveTarget`. In the course of then + * setting that target as the activeTarget, the current activeTarget + * will be instructed to `surrendersActiveTarget`. If the activeTarget + * refuses to surrender, the change is rejected. + */ + activeTarget: { + get: function () { + return this._activeTarget || this.application; + }, + set: function (value) { + + if (!value) { + value = this.application; + } + + if (value === this._activeTarget || (this.activeTarget && !this.activeTarget.surrendersActiveTarget(value))) { + return; + } + + if (value) { + value.willBecomeActiveTarget(this.activeTarget); + this._activeTarget = value; + value.didBecomeActiveTarget(); + } + } + } + +}); + +// Object.defineProperty(exports, "defaultEventManager", { +// get: function () { +// if (typeof defaultEventManager === "undefined") { +// defaultEventManager = new EventManager(); +// if (typeof window !== "undefined") { +// defaultEventManager.initWithWindow(window); +// } +// } +// return defaultEventManager; +// } +// }); + +exports.defaultEventManager = defaultEventManager = new EventManager(); +if (typeof window !== "undefined") { + defaultEventManager.initWithWindow(window); +} diff --git a/core/event/mutable-event.wip.js b/core/event/mutable-event.wip.js new file mode 100644 index 000000000..fe5184afa --- /dev/null +++ b/core/event/mutable-event.wip.js @@ -0,0 +1,734 @@ +/** + @module mod/core/event/mutable-event + @requires montage + */ + const Montage = require("../core").Montage, + uuid = require("../../core/uuid"), + Promise = require("../promise").Promise, + console = require('../extras/console').console; + +var wrapPropertyGetter = function (key, storageKey) { + return function () { + return this.hasOwnProperty(storageKey) ? this[storageKey] : this._event[key]; + }; + }, + wrapPropertySetter = function (storageKey) { + return function (value) { + this[storageKey] = value; + }; + }; + + +// XXX Does not presently function server-side +//if (typeof window !== "undefined") { + + var _eventConstructorsByType = {}; + + var wrapProperty = function (obj, key) { + var storageKey = "_" + key; + + Montage.defineProperty(obj, storageKey, {value: undefined}); + + Montage.defineProperty(obj, key, { + get: wrapPropertyGetter(key, storageKey), + set: wrapPropertySetter(storageKey) + }); + }; + /** + @class MutableEvent + */ + var MutableEvent = exports.MutableEvent = Montage.specialize(/** @lends MutableEvent# */ { + + /** + @private + */ + _initPrototypeWithEvent: { + value: function (event) { + var key, proto = this.__proto__ || Object.getPrototypeOf(this); + + /* jshint forin: true */ + for (key in event) { + /* jshint forin: false */ + + // Don't overwrite keys we have installed + if (key in this || Object.getOwnPropertyDescriptor(proto,key)) { + continue; + } + + // Skip methods, the ones we care about have been wrapped + // already. + // TODO actually wrap all known functions generically + //if (typeof this[key] === "function") { + // continue; + //} + + // TODO ok, maybe it would be quicker to not make this a + // function, but I really hate duplicated code. + wrapProperty(this, key); + } + + wrapProperty(this, "replayed"); + + return this; + } + }, + + _initWithEvent: { + value: function (event) { + this._event = event; + return this; + } + }, + + _id: { + value: undefined + }, + + id: { + get: function() { + return this._id || (this._id = uuid.generate()); + }, + set: function(value) { + if(value !== this._id) { + this._id = value; + } + } + }, + + + /** + * @function + */ + preventDefault: { + value: function () { + this._event + ? this._event.preventDefault() + : (this._defaultPrevented = true); + } + }, + + /** + * @function - deprecated + */ + getPreventDefault: { + value: function () { + if (this._event) { + if (this._event.getPreventDefault) { + return this._event.getPreventDefault(); + } + return this._event.defaultPrevented; + } else { + return this.defaultPrevented; + } + } + }, + + _defaultPrevented: { + value: false + }, + defaultPrevented: { + value: function () { + return this._event + ? this._event.defaultPrevented + : this._defaultPrevented; + } + }, + + /** + * @function + */ + stopImmediatePropagation: { + value: function () { + if(this._event) this._event.stopImmediatePropagation(); + // TODO only if the event is cancellable? + this.propagationStopped = true; + this.immediatePropagationStopped = true; + } + }, + + /** + * @type {Property} + * @default {boolean} false + */ + propagationStopped: { + value: false + }, + + /** + * @type {Property} + * @default {boolean} false + */ + immediatePropagationStopped: { + value: false + }, + + /** + * @type {Property} + * @default {boolean} true + */ + mutable: { + value: true + }, + + /** + * @function + */ + stopPropagation: { + value: function () { + if(this._event) this._event.stopPropagation(); + // TODO only if the event is cancellable? + this.propagationStopped = true; + } + }, + + /** + * @function + */ + stop: { + value: function () { + this.preventDefault(); + this.stopPropagation(); + } + }, + + /** + * A propagation promise is set by the event maanager when a listener returns a promise from handling the event. + * event propagates to the next listener when that promise is fullfilled. + * + * @type {Property} + * @default {Promise} null + */ + propagationPromise: { + value: Promise.resolveUndefined + }, + + + _eventPhase: { + value: void 0 + }, + /** + * @type {Property} + * @default {Element} null + */ + eventPhase: { + get: function () { + return (this._eventPhase !== void 0) + ? this._eventPhase + : this._event + ? this._event.eventPhase + : undefined; + }, + set: function (value) { + this._eventPhase = value; + } + }, + _target: { + value: void 0 + }, + /** + * @type {Property} + * @default {Element} null + */ + target: { + get: function () { + return (this._target !== void 0) + ? this._target + : this._event + ? this._event.target + : undefined; + }, + set: function (value) { + this._target = value; + } + }, + _currentTarget: { + value: void 0 + }, + /** + * @type {Property} + * @default {Element} null + */ + currentTarget: { + get: function () { + return (this._currentTarget !== void 0) + ? this._currentTarget + : this._event + ? this._event.currentTarget + : undefined; + }, + set: function (value) { + this._currentTarget = value; + } + }, + _type: { + value: void 0 + }, + /** + * @type {Property} + * @default {Element} null + */ + type: { + get: function () { + return (this._type !== void 0) ? this._type : this._event.type; + }, + set: function (value) { + this._type = value; + } + }, + _bubbles: { + value: void 0 + }, + /** + * @type {Property} + * @default {Boolean} null + */ + bubbles: { + get: function () { + return (this._bubbles !== void 0) ? this._bubbles : (this._event && this._event.bubbles); + }, + set: function (value) { + this._bubbles = value; + } + }, + /** + * @type {Property} + * @default {Element} null + */ + touches: { + get: function () { + return this._event ? this._event.touches : null; + }, + set: function (value) { + this._event.touches = value; + } + }, + /** + * @type {Property} + * @default {Element} null + */ + changedTouches: { + get: function () { + return this._event ? this._event.changedTouches : null; + }, + set: function (value) { + this._event.changedTouches = value; + } + }, + /** + * @type {Property} + * @default {Element} null + */ + targetTouches: { + get: function () { + return this._event ? this._event.targetTouches : null; + } + }, + + _cancelable: { + value: void 0 + }, + /** + * @type {Property} + * @default {boolean} should be false by default + */ + cancelable: { + get: function () { + return (this._cancelable !== void 0) ? this._cancelable : this._event && this._event.cancelable; + }, + set: function (value) { + this._cancelable = value; + } + }, + + _defaultPrevented: { + value: void 0 + }, + /** + * @type {Property} + * @default {boolean} false + */ + defaultPrevented: { + get: function () { + return (this._defaultPrevented !== void 0) ? this._defaultPrevented : (this._event ? this._event.defaultPrevented : false); + }, + set: function (value) { + this._defaultPrevented = value; + } + }, + _timeStamp: { + value: void 0 + }, + /** + * @type {Property} + * @default {Element} null + */ + timeStamp: { + get: function () { + return (this._timeStamp !== void 0) + ? this._timeStamp + : this._event + ? this._event.timeStamp + : undefined; + }, + set: function (value) { + this._timeStamp = value; + } + }, + _detail: { + value: void 0 + }, + /** + * @type {Property} + * @default {Object} null + */ + detail: { + get: function () { + return (this._detail !== void 0) ? this._detail : (this._event && this._event.detail); + }, + set: function (value) { + this._detail = value; + } + }, + _composedPath: { + value: void 0 + }, + /** + * This is a method call because that's what the standard is... + * @type {Property} + * @default {Element} null + */ + composedPath: { + value: function () { + return this._composedPath; + } + }, + /** + * Becasue Web standards made the "getter" a method call... + * @type {Property} + * @default {Element} null + */ + setComposedPath: { + value: function (value) { + if(value !== this._composedPath) { + this._composedPath = value; + } + } + } + + }, { + wrapEvent: { + value: true + }, + + /** + * @function + * @param {Event} event The original event. + * @returns newEvent + */ + fromEvent: { + value: function fromEvent(event) { + console.groupTime("fromEvent"); + var eventSubclass = _eventConstructorsByType[event.type]; + if (!eventSubclass) { + + if(this.wrapEvent) { + eventSubclass = function MutableEvent() { + }; + _eventConstructorsByType[event.type] = eventSubclass; + eventSubclass.prototype = new exports.MutableEvent()._initPrototypeWithEvent(event); + + } else { + + eventSubclass = class extends event.constructor { + + static { + // const p = this.prototype; + + // /** + // * @type {Property} + // * @default {boolean} false + // */ + // p.propagationStopped = false; + + // /** + // * @type {Property} + // * @default {boolean} false + // */ + // p.immediatePropagationStopped = false; + + // /** + // * @type {Property} + // * @default {Promise} null + // */ + // p.propagationPromise = null; + + + // p._eventPhase = void 0; + // p._target = void 0; + // p._currentTarget = void 0; + // p._type = void 0; + // p._bubbles = void 0; + // p._cancelable = void 0; + // p._defaultPrevented = void 0; + // p._timeStamp = void 0; + // p._detail = void 0; + + + Montage.defineProperties(this.prototype, { + + /** + * @type {Property} + * @default {boolean} false + */ + propagationStopped: {value: false}, + + /** + * @type {Property} + * @default {boolean} false + */ + immediatePropagationStopped: {value: false}, + + /** + * @type {Property} + * @default {Promise} null + */ + propagationPromise: {value: null}, + + _eventPhase: {value: void 0}, + _target: {value: void 0}, + _currentTarget: {value: void 0}, + _type: {value: void 0}, + _bubbles: {value: void 0}, + _cancelable: {value: void 0}, + _defaultPrevented: {value: void 0}, + _timeStamp: {value: void 0}, + _detail: {value: void 0} + }); + + } + /** + * @function + */ + preventDefault() { + super.preventDefault(); + } + + defaultPrevented() { + return super.defaultPrevented; + } + + /** + * @function + */ + stopImmediatePropagation() { + super.stopImmediatePropagation(); + // TODO only if the event is cancellable? + this.propagationStopped = true; + this.immediatePropagationStopped = true; + }; + + + /** + * @function + */ + stopPropagation() { + super.stopPropagation(); + // TODO only if the event is cancellable? + this.propagationStopped = true; + } + + /** + * @function + */ + stop() { + this.preventDefault(); + this.stopPropagation(); + } + + + /** + * @type {Property} + * @default {Element} null + */ + get eventPhase() { + return (this._eventPhase !== void 0) + ? this._eventPhase + : super.eventPhase; + } + set eventPhase(value) { + this._eventPhase = value; + } + + + /** + * @type {Property} + * @default {Element} null + */ + get target() { + return (this._target !== void 0) + ? this._target + : super.target; + } + set target(value) { + this._target = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get currentTarget() { + return (this._currentTarget !== void 0) + ? this._currentTarget + : super.currentTarget; + } + set currentTarget(value) { + this._currentTarget = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get type() { + return (this._type !== void 0) ? this._type : super.type; + } + set type (value) { + this._type = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get bubbles() { + return (this._bubbles !== void 0) + ? this._bubbles + : super.bubbles; + } + set bubbles(value) { + this._bubbles = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get touches () { + return this.touches; + } + set touches (value) { + this.touches = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get changedTouches () { + return super.changedTouches; + } + set changedTouches (value) { + this.changedTouches = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get targetTouches() { + return this.targetTouches; + } + + /** + * @type {Property} + * @default {boolean} should be false by default + */ + get cancelable () { + return (this._cancelable !== void 0) ? this._cancelable : super.cancelable; + } + set cancelable (value) { + this._cancelable = value; + } + + /** + * @type {Property} + * @default {boolean} false + */ + get defaultPrevented () { + return (this._defaultPrevented !== void 0) + ? this._defaultPrevented + : super.defaultPrevented; + } + set defaultPrevented (value) { + this._defaultPrevented = value; + } + + /** + * @type {Property} + * @default {Element} null + */ + get timeStamp () { + return (this._timeStamp !== void 0) + ? this._timeStamp + : super.timeStamp; + } + set timeStamp(value) { + this._timeStamp = value; + } + + /** + * @type {Property} + * @default {Object} null + */ + get detail() { + return (this._detail !== void 0) + ? this._detail + : super.detail; + } + set detail(value) { + this._detail = value; + } + + }; + _eventConstructorsByType[event.type] = eventSubclass; + + } + } + var result = this.wrapEvent + ? new eventSubclass()._initWithEvent(event) + : (Object.setPrototypeOf(event,eventSubclass.prototype) && event); + console.groupTimeEnd("fromEvent"); + return result; + + } + }, + + // Same arguments as initEvent & initCustomEvent + + /** + * @function + * @param {Event} type TODO + * @param {Event} canBubbleArg TODO + * @param {Event} cancelableArg TODO + * @param {Event} data TODO + * @returns this.fromEvent(anEvent) + */ + fromType: { + value: function MutableEvent_fromType(type, canBubbleArg, cancelableArg, detail) { + var newEvent = new this(); + + newEvent.type = type; + newEvent.bubbles = typeof canBubbleArg === "boolean" ? canBubbleArg : false; + newEvent.cancelable = typeof cancelableArg === "boolean" ? cancelableArg : false; + if(detail) newEvent.detail = detail; + + return newEvent; + + //return this.fromEvent(new CustomEvent(type, {bubbles: canBubbleArg, cancelable:cancelableArg, detail:detail})); + } + } + + }); + +//} // client-side diff --git a/data/model/data-identifier.js b/data/model/data-identifier.js index 651e361fc..1562f53a3 100644 --- a/data/model/data-identifier.js +++ b/data/model/data-identifier.js @@ -44,6 +44,7 @@ exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype /** * The ObjectDescriptor associated with a dataIdentifier if available + * TODO: Rename to typeDescriptor * * @type {ObjectDescriptor} */ diff --git a/data/model/data-identifier.mjson b/data/model/data-identifier.mjson index e619faf51..803d20b53 100644 --- a/data/model/data-identifier.mjson +++ b/data/model/data-identifier.mjson @@ -27,16 +27,12 @@ "DataIdentifier": { "object": "./data-identifier" }, - "DataServiceDescriptor": { - "object": "data/service/data-service.mjson" - }, "dataService": { "prototype": "core/meta/property-descriptor", "values": { "name": "dataService", "valueType": "object", - "cardinality": 1, - "valueDescriptor": {"@": "DataServiceDescriptor"} + "cardinality": 1 } }, "ObjectDescriptorDescriptor": { diff --git a/data/model/data-object.js b/data/model/data-object.js index f5ffafe06..a44e13165 100644 --- a/data/model/data-object.js +++ b/data/model/data-object.js @@ -2,6 +2,8 @@ const DataEvent = require("./data-event").DataEvent; const Date = require("core/extras/date").Date; const Montage = require("core/core").Montage; const Target = require("core/target").Target; +const application = require("core/application").application; + /** * @class DataObject @@ -26,6 +28,14 @@ exports.DataObject = class DataObject extends Target { */ fullModuleId: { value: undefined }, + // /** + // * an instance's dataIdentifier + // * + // * @property {DataIdentifier} + // * @default null + // */ + // dataIdentifier: { value: undefined }, + /** * Stores the latest known data snapshot from an origin service it was imported from. * An entry per origin data service contains the data snapshot from that origin @@ -288,6 +298,55 @@ exports.DataObject = class DataObject extends Target { } } + // serializeSelf(serializer) { + // if (super.deserializeSelf) { + // super.deserializeSelf(deserializer); + // } + + // // Can't use this unless we know that this.objectDescriptor is available to us + // if (this.objectDescriptor) { + // let objectDescriptor = this.objectDescriptor, + // propertyDescriptors = objectDescriptor.propertyDescriptors, + // ownPropertyNames = Object.keys(this); + + // for (let i=0, iPropertyDescriptor, iCount = ownPropertyNames.length; i < iCount; i++) { + // //for (let iPropertyDescriptor of propertyDescriptors) { + // if (((iPropertyDescriptor = objectDescriptor.propertyDescriptorNamed(ownPropertyNames[i])) !== null) && iPropertyDescriptor.isSerializable !== false && this[iPropertyDescriptor.name] !== undefined) { + // serializer.setProperty(iPropertyName, this[iPropertyName]); + // } + // } + // } else { + + // if (this.originId) { + // serializer.setProperty("originId", this.originId); + // } + // if (this.originDataSnapshot) { + // serializer.setProperty("originDataSnapshot", this.originDataSnapshot); + // } + + // if (this.isType !== undefined) { + // serializer.setProperty("isType", this.isType); + // } + // if (this.isTemplate !== undefined) { + // serializer.setProperty("isTemplate", this.isTemplate); + // } + + // if (this.description) { + // serializer.setProperty("description", this.description); + // } + // if (this.creationDate) { + // serializer.setProperty("creationDate", this.creationDate); + // } + // if (this.modificationDate) { + // serializer.setProperty("modificationDate", this.modificationDate); + // } + // if (this.publicationDate) { + // serializer.setProperty("publicationDate", this.publicationDate); + // } + // } + // } + + clone() { return this; } @@ -309,16 +368,45 @@ exports.DataObject = class DataObject extends Target { */ static prepareToHandleDataEvents(event) { - event.dataService.objectDescriptorForType(this).addEventListener(DataEvent.create, this, false); + event.dataService.objectDescriptorForType(this).addEventListener(DataEvent.willSave, this, false); } - static handleCreate(event) { - // if(event.dataObject instanceof this) { - event.dataObject.creationDate = event.dataObject.modificationDate = new Date(); - // } - } + // static handleCreate(event) { + // // if(event.dataObject instanceof this) { + // event.dataObject.creationDate = event.dataObject.modificationDate = new Date(); + // event.dataObject.creationIdentity = application.identity; + // // } + // } + + static handleWillSave(event) { + const objectDescriptor = event.target, + transaction = event.detail, + createdDataObjects = transaction.createdDataObjects.get(objectDescriptor), + updatedDataObjects = transaction.updatedDataObjects.get(objectDescriptor), + deletedDataObjects = transaction.deletedDataObjects.get(objectDescriptor), + nowDate = new Date(), + identity = transaction.identity; + + if(createdDataObjects) { + for(let instance of createdDataObjects) { + instance.creationDate = instance.modificationDate = nowDate; + instance.creationIdentity = instance.modificationIdentity = identity; + } + } + + if(updatedDataObjects) { + for(let instance of updatedDataObjects) { + instance.modificationDate = nowDate; + instance.modificationIdentity = identity; + } + } + + if(deletedDataObjects) { + for(let instance of deletedDataObjects) { + instance.archivalDate = nowDate; + instance.archivalIdentity = identity; + } + } - static handleUpdate(event) { - event.dataObject.modificationDate = new Date(); } }; diff --git a/data/model/data-object.mjson b/data/model/data-object.mjson index 123fe9ba5..1aa653986 100644 --- a/data/model/data-object.mjson +++ b/data/model/data-object.mjson @@ -5,6 +5,7 @@ "name": "DataObject", "propertyDescriptors": [ {"@": "fullModuleId"}, + {"@": "dataIdentifier"}, {"@": "date"}, {"@": "originId"}, {"@": "originDataSnapshot"}, @@ -53,6 +54,19 @@ } }, + "DataIdentifierDescriptor": { + "object": "./data-identifier.mjson" + }, + + "dataIdentifier": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "dataIdentifier", + "valueDescriptor": {"@": "DataIdentifierDescriptor"}, + "description": "The data identifier unique to that object" + } + }, + "type": { "prototype": "core/meta/property-descriptor", "values": { diff --git a/data/model/transaction.js b/data/model/transaction.js index 3c203a06f..e2f1b99b2 100644 --- a/data/model/transaction.js +++ b/data/model/transaction.js @@ -1,7 +1,8 @@ var Montage = require("../../core/core").Montage, + Target = require("core/target").Target, + DataObjectDescriptor = require("./data-object.mjson").montageObject, uuid = require("../../core/uuid"), - DataService = require("../service/data-service").DataService, - Transaction; + DataService = require("../service/data-service").DataService; /** * A Transaction represents a unit of changes grouped together and intend to be saved at the same time. @@ -9,300 +10,704 @@ var Montage = require("../../core/core").Montage, * @class * @extends external:Montage */ - Transaction = exports.Transaction = Montage.specialize(/** @lends Transaction.prototype */ { - constructor: { - value: function Transaction() { - this.identifier = uuid.generate(); - this._completionPromiseFunctionsByParticipant = new Map(); - return this; - } - }, - - identifier: { - value: undefined - }, - - /** - * Returns a Set containing ObjectDescriptors involved in the transaction across all type of changes - * - * @type {Set} - */ - objectDescriptors: { - value: undefined - }, - - _completionPromiseFunctionsByParticipant: { - value: undefined - }, - - createCompletionPromiseForParticipant: { - value: function(participant) { - var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(participant), - self = this; - - if(!participationPromiseArguments) { - var participantCompletionPromise = new Promise(function(resolve, reject) { - self._completionPromiseFunctionsByParticipant.set(participant,arguments); - }); - this.participantCompletionPromises.push(participantCompletionPromise); - } - } - }, - - resolveCompletionPromiseForParticipant: { - value: function(participant) { - var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); - if(promiseFunctions) { - promiseFunctions[0](participant); - if(this._completionAllSettledValues) { - this._completionAllSettledValues.push({status: "fulfilled", value: participant}); + +const Transaction = exports.Transaction = class Transaction extends Target { + /** @lends Transaction */ + constructor() { + super(); + + this.identifier = uuid.generate(); + this._completionPromiseFunctionsByParticipant = new Map(); + DataObjectDescriptor.addEventListener("change", this, false); + return this; + } + + static { + Montage.defineProperties(this.prototype, { + // apiVersion: { + // value: "FROM AWS, NECESSARY FOR GCP?" + // } + identifier: { + value: undefined + }, + /** + * The identity instance representing the user behind the changes grouped in the transaction. + * That identity is carried through the data operation created by the data layer to process it. + * + * @type {mod/data/identity} + */ + identity: { + value: undefined + }, + + /** + * Returns a Set containing ObjectDescriptors involved in the transaction across all type of changes + * + * @type {Set} + */ + _objectDescriptors: { + value: undefined + }, + objectDescriptors: { + get: function() { + //No caching for now, if we do, we'd have to observe this.createdDataObjects/updatedDataObjects/deletedDataObjects to keep it accurate + let resultSet; + + if(this.createdDataObjects.size) { + (resultSet || (resultSet = new Set())).addEach(this.createdDataObjects.keysArray()) + } + if(this.updatedDataObjects.size) { + (resultSet || (resultSet = new Set())).addEach(this.updatedDataObjects.keysArray()) + } + if(this.deletedDataObjects.size) { + (resultSet || (resultSet = new Set())).addEach(this.deletedDataObjects.keysArray()) + } + + return resultSet; + + return new Set(...this.createdDataObjects.keysArray(), ...this.updatedDataObjects.keysArray(), ...this.deletedDataObjects.keysArray()) + // return this._objectDescriptors; + }, + set: function(value) { + console.warn("Transaction.objectDescriptors is now computed on-demand from the content of this.createdDataObjects/updatedDataObjects/deletedDataObjects and not cached") + if(value !== this._objectDescriptors) { + this._objectDescriptors = value; + } } - } - } - }, - rejectCompletionPromiseForParticipantWithError: { - value: function(participant, error) { - var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); - if(promiseFunctions) { - promiseFunctions[1](error); - if(this._completionAllSettledValues) { - this._completionAllSettledValues.push({status: "rejected", reason: error}); + }, + + + _completionPromiseFunctionsByParticipant: { + value: undefined + }, + + createCompletionPromiseForParticipant: { + value: function(participant) { + var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(participant), + self = this; + + if(!participationPromiseArguments) { + var participantCompletionPromise = new Promise(function(resolve, reject) { + self._completionPromiseFunctionsByParticipant.set(participant,arguments); + }); + this.participantCompletionPromises.push(participantCompletionPromise); + } } - } - } - }, - - clearCompletionPromises: { - value: function(participant) { - this._participantCompletionPromises.length = 0; - this._completionPromiseFunctionsByParticipant.clear(); - this._completionPromise = null; - } - }, - - - _participantCompletionPromises: { - value: undefined - }, - - participantCompletionPromises: { - get: function() { - return this._participantCompletionPromises || (this._participantCompletionPromises = []); - } - }, - - _completionPromise: { - value: undefined - }, - _completionPromiseFunctions: { - value: undefined - }, - - /* - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled - [ - {status: "fulfilled", value: 33}, - {status: "fulfilled", value: 66}, - {status: "fulfilled", value: 99}, - {status: "rejected", reason: Error: an error} - ] - */ - _completionAllSettledValues: { - value: undefined - }, - - completionPromise: { - get: function() { - if(!this._completionPromise) { - /* - We don't really know when we're done receiving createCompletionPromiseForParticipant - So it's dicy, if someonce calls completionPromise before it's over, stuff will be missed... - */ - if(this.participantCompletionPromises && this.participantCompletionPromises.length) { - this._completionPromise = Promise.all(this.participantCompletionPromises); - - var self = this; - this._completionPromise.finally(function() { - self.clearCompletionPromises(); - }) - } else { - this._completionPromise = Promise.resolve(null); - } - // var self = this; - // this._completionPromise = new Promise(function(resolve, reject) { - // self._completionAllSettledValues = []; - // self._completionPromiseFunctions = arguments; - // }); - } - return this._completionPromise; - } - }, - - - // _performCompletionPromises: { - // value: undefined - // }, - - // performCompletionPromises: { - // get: function() { - // return this._performCompletionPromises || (this._performCompletionPromises = []); - // } - // }, - - // performCompletionPromise: { - // value: undefined - // }, - - - // rawDataServiceSucceeded: { - // value: function(aRawDataService) { - // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); - - // //Execute's the promise's resolve function, we don't care about the - // participationPromiseArguments[0](); - // } - // }, - - // rawDataServiceFailedWithError: { - // value: function(aRawDataService, error) { - // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); - - // //Execute's the promise's reject function with the error: - // participationPromiseArguments[1](error); - // } - // }, - - - - /** - * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are created in that transaction - * - * @type {Map} - */ - createdDataObjects: { - value: undefined - }, - - /** - * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are changed in that transaction - * - * @type {Map} - */ - updatedDataObjects: { - value: undefined - }, - - /** - * A Map where keys are ObjectDescriptors and values are maps where criteria - * matching dataObject instances with changes are the keys, and values are the changes - * - * @type {Map} - */ - updatedData: { - value: undefined - }, - - - /** - * A Map where keys are dataObjects and values are changes for a dataObject that will be saved within the transaction. - * - * @type {Map} - */ - dataObjectChanges: { - value: undefined - }, - - /** - * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are deleted in that transaction - * - * @type {Map} - */ - deletedDataObjects: { - value: undefined - }, - - /** - * A Map where keys are ObjectDescriptors and values are Sets containing criteria - * describing dataObject instances to be deleted, or null if all instances are to be deleted - * - * @type {Map} - */ - deletetedData: { - value: undefined - }, - - /** - * A Map - * - * @type {Map} - */ - - dataOperationsByObject: { - value: undefined - }, - - - deserializeSelf: { - value: function (deserializer) { - this.super(deserializer); - - var result, value; - value = deserializer.getProperty("identifier"); - if (value !== void 0) { - this.identifier = value; - } + }, + + resolveCompletionPromiseForParticipant: { + value: function(participant) { + var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); + if(promiseFunctions) { + promiseFunctions[0](participant); + if(this._completionAllSettledValues) { + this._completionAllSettledValues.push({status: "fulfilled", value: participant}); + } + } + } + }, + rejectCompletionPromiseForParticipantWithError: { + value: function(participant, error) { + var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); + if(promiseFunctions) { + promiseFunctions[1](error); + if(this._completionAllSettledValues) { + this._completionAllSettledValues.push({status: "rejected", reason: error}); + } + } + } + }, - value = deserializer.getProperty("objectDescriptorModuleIds"); - if (value !== void 0) { - var mainService = DataService.mainService, - i, iObjectDescriptorModuleId, countI, - objectDescriptors = []; - - for(i=0, countI = value.length; (i {return objectDescriptor.module.id}); - serializer.setProperty("objectDescriptorModuleIds", objectDescriptorModuleIds); - } + // rawDataServiceSucceeded: { + // value: function(aRawDataService) { + // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); + // //Execute's the promise's resolve function, we don't care about the + // participationPromiseArguments[0](); + // } + // }, - // if(this.applicationCredentials) { - // serializer.setProperty("applicationCredentials", this.applicationCredentials); - // } + // rawDataServiceFailedWithError: { + // value: function(aRawDataService, error) { + // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); - // if(this.scope) { - // serializer.setProperty("scope", this.scope); - // } - } - }, + // //Execute's the promise's reject function with the error: + // participationPromiseArguments[1](error); + // } + // }, + + + + /** + * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are created in that transaction + * + * @type {Map} + */ + createdDataObjects: { + value: undefined + }, + + isObjectCreated: { + value: function (object) { + return !!this.createdDataObjects?.get(object.objectDescriptor)?.has(object); + } + }, + + /** + * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are changed in that transaction + * + * @type {Map} + */ + updatedDataObjects: { + value: undefined + }, + + /** + * A Map where keys are ObjectDescriptors and values are maps where criteria + * matching dataObject instances with changes are the keys, and values are the changes + * + * @type {Map} + */ + updatedData: { + value: undefined + }, + + + /** + * A Map where keys are dataObjects and values are changes for a dataObject that will be saved within the transaction. + * + * @type {Map} + */ + dataObjectChanges: { + value: undefined + }, + + _buildChangesForDataObject: { + value: function (dataObject) { + let changesForDataObject = new Map(); + this.dataObjectChanges.set(dataObject, changesForDataObject); + return changesForDataObject; + }, + }, + + changesForDataObject: { + value: function (dataObject) { + return this.dataObjectChanges.get(dataObject); + }, + }, + + /** + * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are deleted in that transaction + * + * @type {Map} + */ + deletedDataObjects: { + value: undefined + }, + + /** + * A Map where keys are ObjectDescriptors and values are Sets containing criteria + * describing dataObject instances to be deleted, or null if all instances are to be deleted + * + * @type {Map} + */ + deletetedData: { + value: undefined + }, + + /** + * A Map + * + * @type {Map} + */ + + dataOperationsByObject: { + value: undefined + }, + + + deserializeSelf: { + value: function (deserializer) { + this.super(deserializer); + + var result, value; + value = deserializer.getProperty("identifier"); + if (value !== void 0) { + this.identifier = value; + } + + value = deserializer.getProperty("objectDescriptorModuleIds"); + if (value !== void 0) { + var mainService = DataService.mainService, + i, iObjectDescriptorModuleId, countI, + objectDescriptors = []; + + for(i=0, countI = value.length; (i {return objectDescriptor.module.id}); + + serializer.setProperty("objectDescriptorModuleIds", objectDescriptorModuleIds); + } + + + // if(this.applicationCredentials) { + // serializer.setProperty("applicationCredentials", this.applicationCredentials); + // } + + // if(this.scope) { + // serializer.setProperty("scope", this.scope); + // } + } + }, + + handleChange: { + value: function (changeEvent) { + //We're going to add ourselve as context for the mainService to better know what to do + //This is convoluted... but as data-service.js requires this file... + // let mainService = changeEvent.target.dataIdentifier.dataService.mainService; + changeEvent.transaction = this; + } + } -}); + }); + } +}; + + + +// Transaction = exports.Transaction = Montage.specialize(/** @lends Transaction.prototype */ { +// constructor: { +// value: function Transaction() { +// this.identifier = uuid.generate(); +// this._completionPromiseFunctionsByParticipant = new Map(); +// this.addEventListener("change", this, false); +// return this; +// } +// }, + +// identifier: { +// value: undefined +// }, + +// /** +// * The identity instance representing the user behind the changes grouped in the transaction. +// * That identity is carried through the data operation created by the data layer to process it. +// * +// * @type {mod/data/identity} +// */ +// identity: { +// value: undefined +// }, + +// /** +// * Returns a Set containing ObjectDescriptors involved in the transaction across all type of changes +// * +// * @type {Set} +// */ +// objectDescriptors: { +// value: undefined +// }, + +// _completionPromiseFunctionsByParticipant: { +// value: undefined +// }, + +// createCompletionPromiseForParticipant: { +// value: function(participant) { +// var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(participant), +// self = this; + +// if(!participationPromiseArguments) { +// var participantCompletionPromise = new Promise(function(resolve, reject) { +// self._completionPromiseFunctionsByParticipant.set(participant,arguments); +// }); +// this.participantCompletionPromises.push(participantCompletionPromise); +// } +// } +// }, + +// resolveCompletionPromiseForParticipant: { +// value: function(participant) { +// var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); +// if(promiseFunctions) { +// promiseFunctions[0](participant); +// if(this._completionAllSettledValues) { +// this._completionAllSettledValues.push({status: "fulfilled", value: participant}); +// } +// } +// } +// }, +// rejectCompletionPromiseForParticipantWithError: { +// value: function(participant, error) { +// var promiseFunctions = this._completionPromiseFunctionsByParticipant.get(participant); +// if(promiseFunctions) { +// promiseFunctions[1](error); +// if(this._completionAllSettledValues) { +// this._completionAllSettledValues.push({status: "rejected", reason: error}); +// } +// } +// } +// }, + +// clearCompletionPromises: { +// value: function(participant) { +// this._participantCompletionPromises.length = 0; +// this._completionPromiseFunctionsByParticipant.clear(); +// this._completionPromise = null; +// } +// }, + + +// _participantCompletionPromises: { +// value: undefined +// }, + +// participantCompletionPromises: { +// get: function() { +// return this._participantCompletionPromises || (this._participantCompletionPromises = []); +// } +// }, + +// _completionPromise: { +// value: undefined +// }, +// _completionPromiseFunctions: { +// value: undefined +// }, + +// /* +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled +// [ +// {status: "fulfilled", value: 33}, +// {status: "fulfilled", value: 66}, +// {status: "fulfilled", value: 99}, +// {status: "rejected", reason: Error: an error} +// ] +// */ +// _completionAllSettledValues: { +// value: undefined +// }, + +// completionPromise: { +// get: function() { +// if(!this._completionPromise) { +// /* +// We don't really know when we're done receiving createCompletionPromiseForParticipant +// So it's dicy, if someonce calls completionPromise before it's over, stuff will be missed... +// */ +// if(this.participantCompletionPromises && this.participantCompletionPromises.length) { +// this._completionPromise = Promise.all(this.participantCompletionPromises); + +// var self = this; +// this._completionPromise.finally(function() { +// self.clearCompletionPromises(); +// }) +// } else { +// this._completionPromise = Promise.resolve(null); +// } +// // var self = this; +// // this._completionPromise = new Promise(function(resolve, reject) { +// // self._completionAllSettledValues = []; +// // self._completionPromiseFunctions = arguments; +// // }); +// } +// return this._completionPromise; +// } +// }, + + +// // _performCompletionPromises: { +// // value: undefined +// // }, + +// // performCompletionPromises: { +// // get: function() { +// // return this._performCompletionPromises || (this._performCompletionPromises = []); +// // } +// // }, + +// // performCompletionPromise: { +// // value: undefined +// // }, + + +// // rawDataServiceSucceeded: { +// // value: function(aRawDataService) { +// // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); + +// // //Execute's the promise's resolve function, we don't care about the +// // participationPromiseArguments[0](); +// // } +// // }, + +// // rawDataServiceFailedWithError: { +// // value: function(aRawDataService, error) { +// // var participationPromiseArguments = this._completionPromiseFunctionsByParticipant.get(aRawDataService); + +// // //Execute's the promise's reject function with the error: +// // participationPromiseArguments[1](error); +// // } +// // }, + + + +// /** +// * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are created in that transaction +// * +// * @type {Map} +// */ +// createdDataObjects: { +// value: undefined +// }, + +// isObjectCreated: { +// value: function (object) { +// return !!this.createdDataObjects?.get(object.objectDescriptor)?.has(object); +// } +// }, + +// /** +// * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are changed in that transaction +// * +// * @type {Map} +// */ +// updatedDataObjects: { +// value: undefined +// }, + +// /** +// * A Map where keys are ObjectDescriptors and values are maps where criteria +// * matching dataObject instances with changes are the keys, and values are the changes +// * +// * @type {Map} +// */ +// updatedData: { +// value: undefined +// }, + + +// /** +// * A Map where keys are dataObjects and values are changes for a dataObject that will be saved within the transaction. +// * +// * @type {Map} +// */ +// dataObjectChanges: { +// value: undefined +// }, + +// changesForDataObject: { +// value: function (dataObject) { +// return this.dataObjectChanges.get(dataObject); +// }, +// }, + +// /** +// * A Map where keys are ObjectDescriptors and values are matching dataObject instances that are deleted in that transaction +// * +// * @type {Map} +// */ +// deletedDataObjects: { +// value: undefined +// }, + +// /** +// * A Map where keys are ObjectDescriptors and values are Sets containing criteria +// * describing dataObject instances to be deleted, or null if all instances are to be deleted +// * +// * @type {Map} +// */ +// deletetedData: { +// value: undefined +// }, + +// /** +// * A Map +// * +// * @type {Map} +// */ + +// dataOperationsByObject: { +// value: undefined +// }, + + +// deserializeSelf: { +// value: function (deserializer) { +// this.super(deserializer); + +// var result, value; +// value = deserializer.getProperty("identifier"); +// if (value !== void 0) { +// this.identifier = value; +// } + +// value = deserializer.getProperty("objectDescriptorModuleIds"); +// if (value !== void 0) { +// var mainService = DataService.mainService, +// i, iObjectDescriptorModuleId, countI, +// objectDescriptors = []; + +// for(i=0, countI = value.length; (i {return objectDescriptor.module.id}); + +// serializer.setProperty("objectDescriptorModuleIds", objectDescriptorModuleIds); +// } + + +// // if(this.applicationCredentials) { +// // serializer.setProperty("applicationCredentials", this.applicationCredentials); +// // } + +// // if(this.scope) { +// // serializer.setProperty("scope", this.scope); +// // } +// } +// }, + +// handleChange: { +// value: function (changeEvent) { +// //We're going to add ourselve as context for the mainService to better know what to do +// //This is convoluted... but as data-service.js requires this file... +// // let mainService = changeEvent.target.dataIdentifier.dataService.mainService; +// changeEvent.transaction = this; +// } +// } + +// }); diff --git a/data/service/data-service.js b/data/service/data-service.js index f503f36f1..40ee0f55b 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2313,8 +2313,13 @@ DataService.addClassProperties( if (promise) { return promise.then(function () { var result = null; - if (propertiesToRequest.length && object[propertyName]) { - result = self._getPropertiesOnPath(object[propertyName], propertiesToRequest); + if (propertiesToRequest.length) { + if(object[propertyName]) { + result = self._getPropertiesOnPath(object[propertyName], propertiesToRequest); + } + } else { + //We're at the end of propertiesToRequest: + result = object[propertyName]; } return result; }); @@ -3000,6 +3005,17 @@ DataService.addClassProperties( __dataEventPoolByEventType: { value: null, }, + _resetDataEvent: { + value: function(dataEvent) { + //The rest is overriden every time + //For willSave... + delete dataEvent["_composedPath"]; + + //For change event... Needs a different reset function per type... + delete dataEvent["transaction"]; + + } + }, _dataEventPoolForEventType: { value: function (eventType) { var pool = (this.__dataEventPoolByEventType || (this.__dataEventPoolByEventType = new Map())).get( @@ -3008,7 +3024,7 @@ DataService.addClassProperties( if (!pool) { this.__dataEventPoolByEventType.set( eventType, - (pool = new ObjectPool(this._eventPoolFactoryForEventType)) + (pool = new ObjectPool(this._eventPoolFactoryForEventType, this._resetDataEvent)) ); } return pool; @@ -3037,7 +3053,7 @@ DataService.addClassProperties( }, dispatchDataEventTypeForObject: { - value: function (eventType, object, detail) { + value: function (eventType, object, detail, composedPath) { /* This needs to be made more generic in EventManager, which has "prepareForActivationEvent, but it's very specialized for components. Having all prototypes of DO register as eventListeners upfront @@ -3054,6 +3070,9 @@ DataService.addClassProperties( dataEvent.dataService = this; dataEvent.dataObject = object; dataEvent.detail = detail; + if(composedPath && dataEvent.setComposedPath) { + dataEvent.setComposedPath(composedPath); + } if (!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); @@ -3690,6 +3709,13 @@ DataService.addClassProperties( let objectDescriptor = this.objectDescriptorForObject(dataObject); let childServicesForType = this.childServicesForType(objectDescriptor); + /* + FIXME: When DataWorker attempts to mergeDataObject for the WebSocketSession, it fails here as this.handlesType(objectDescriptor) is false + so !this.handlesType(objectDescriptor) is true and it just bails out in that if. + + And saveChanges does nothing since merge has not happened.... + + */ if ( !objectDescriptor || (objectDescriptor && @@ -3909,26 +3935,27 @@ DataService.addClassProperties( }, }, registerChangedDataObject: { - value: function (dataObject) { + value: function (dataObject, _changedDataObjects, _objectDescriptorsWithChanges) { var objectDescriptor = this.objectDescriptorForObject(dataObject), changedDataObjects, value; if (this.isObjectCreated(dataObject)) { - console.warn( - `DataService can't register a new object (${objectDescriptor.name}) in changedDataObjects` - ); + // console.warn( + // `DataService can't register a new object (${objectDescriptor.name}) in changedDataObjects` + // ); return; } - changedDataObjects = this.changedDataObjects; + changedDataObjects = _changedDataObjects ? _changedDataObjects : this.changedDataObjects; value = changedDataObjects.get(objectDescriptor); if (!value) { changedDataObjects.set(objectDescriptor, (value = new Set())); } value.add(dataObject); - this.objectDescriptorsWithChanges.add(objectDescriptor); + + (_objectDescriptorsWithChanges ? _objectDescriptorsWithChanges : this.objectDescriptorsWithChanges).add(objectDescriptor); }, }, @@ -3938,6 +3965,10 @@ DataService.addClassProperties( }, }, + /* + This isn't used right now, but when we start using the undo manager + we probably will need to invoke it + */ unregisterChangedDataObject: { value: function (dataObject) { var objectDescriptor = this.objectDescriptorForObject(dataObject), @@ -4547,17 +4578,23 @@ DataService.addClassProperties( shouldTrackChangesWhileBeingMapped ) { var dataObject = changeEvent.target, + transaction = changeEvent.transaction, isCreatedObject = this.isObjectCreated(dataObject), key = changeEvent.key, keyValue = changeEvent.keyValue, addedValues = changeEvent.addedValues, removedValues = changeEvent.removedValues, isDataObjectBeingMapped = this._objectsBeingMapped.has(dataObject), - changesForDataObject = this.changesForDataObject(dataObject), + changesForDataObject = transaction ? transaction.changesForDataObject(dataObject): this.changesForDataObject(dataObject), //WARNING TEST: THIS WAS REDEFINING THE PASSED ARGUMENT //inversePropertyDescriptor, self = this; + + if(transaction) { + shouldTrackChangesWhileBeingMapped = true; + } + /* Benoit refactoring saveChanges: shouldn't we be able to know that if there are no changesForDataObject, as we create on, it would ve the only time we'd have to call: @@ -4572,7 +4609,9 @@ DataService.addClassProperties( if (!isCreatedObject && (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped)) { //this.changedDataObjects.add(dataObject); - this.registerChangedDataObject(dataObject); + changeEvent.transaction + ? this.registerChangedDataObject(dataObject, changesForDataObject, transaction.objectDescriptors) + : this.registerChangedDataObject(dataObject); } //Now handled in changesForDataObject @@ -4615,9 +4654,13 @@ DataService.addClassProperties( changeEvent.key && typeof changeEvent.keyValue !== "undefined" && key !== "length" && - /* new for blocking re-entrant */ changesForDataObject.get(key) !== keyValue + /* new for blocking re-entrant */ changesForDataObject?.get(key) !== keyValue ) { - if (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped) { + //If an object is created, it already has the values on itself, no need to do anything + if (!isCreatedObject && (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped)) { + if(!changesForDataObject) { + changesForDataObject = (transaction ? transaction._buildChangesForDataObject(dataObject) : this._buildChangesForDataObject(dataObject)); + } changesForDataObject.set(key, keyValue); } @@ -4663,7 +4706,7 @@ DataService.addClassProperties( //For key that can have add/remove the value of they key is an object //that itself has two keys: addedValues and removedValues //which value will be a set; - var manyChanges = changesForDataObject.get(key), + var manyChanges = changesForDataObject?.get(key), isManyChangesArray = manyChanges && isArray(manyChanges), i, countI; @@ -4680,10 +4723,10 @@ DataService.addClassProperties( // The index captured in dataObjectChanges is the one from the first change event, so 2. // The indices at which to make the 2nd and 3rd changes are lost. - if (!manyChanges) { - manyChanges = new CollectionChanges(); - changesForDataObject.set(key, manyChanges); - } + // if (!manyChanges) { + // manyChanges = new CollectionChanges(); + // changesForDataObject.set(key, manyChanges); + // } if (isManyChangesArray && (isCreatedObject || isDataObjectBeingMapped)) { @@ -4707,6 +4750,15 @@ DataService.addClassProperties( } } else { + if (!manyChanges) { + manyChanges = new CollectionChanges(); + if(!changesForDataObject) { + changesForDataObject = (transaction ? transaction._buildChangesForDataObject(dataObject) : this._buildChangesForDataObject(dataObject)); + } + changesForDataObject.set(key, manyChanges); + } + + if (addedValues && isManyChangesArray && manyChanges.equals(addedValues)) { manyChanges = new CollectionChanges(); changesForDataObject.set(key, manyChanges); @@ -5291,6 +5343,11 @@ DataService.addClassProperties( saveChanges: { value: function () { + return this.saveChangesWithIdentity(this.application.identity); + } + }, + saveChangesWithIdentity: { + value: function (identity) { //If nothing to do, we bail out as early as possible. if ( this.createdDataObjects.size === 0 && @@ -5313,6 +5370,10 @@ DataService.addClassProperties( noOpOperation.type = DataOperation.Type.NoOp; return Promise.resolve(noOpOperation); } + } else { + if(this._pendingTransactions?.length > 1) { + console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + ">>>>>>>>> saveChangesWithIdentity called: #WARNING WITH _pendingTransactions! !!!!"); + } } var transaction = new Transaction(), @@ -5322,10 +5383,19 @@ DataService.addClassProperties( createdDataObjects = (transaction.createdDataObjects = new Map(this.createdDataObjects)), //Map changedDataObjects = (transaction.updatedDataObjects = new Map(this.changedDataObjects)), //Map deletedDataObjects = (transaction.deletedDataObjects = new Map(this.deletedDataObjects)), //Map - dataObjectChanges = (transaction.dataObjectChanges = new Map(this.dataObjectChanges)), //Map - objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( - this.objectDescriptorsWithChanges - )); + dataObjectChanges = (transaction.dataObjectChanges = new Map(this.dataObjectChanges)); //Map + // objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( + // this.objectDescriptorsWithChanges + // )); + + /* + Properly set the transaction's identity property. + This could allow to implement an "super user" feature. Adding the ability for someone else + to authenticate, creating a second session with a different identity, to do something the current user + wouldn't be allowed to for example. + */ + transaction.identity = identity; + //console.log("saveChanges: transaction-"+this.identifier, transaction); console.log( @@ -5348,7 +5418,24 @@ DataService.addClassProperties( // this.dataObjectChanges.clear(); // this.objectDescriptorsWithChanges.clear(); - return this._saveChangesForTransaction(transaction); + if(this.pendingTransactionPromise) { + console.debug("!!!!!!!!!!!!!! Pending transaction holding transaction "+transaction.identifier); + this.pendingTransactionPromise = this.pendingTransactionPromise.then(() => { + console.debug("+++++++++++++++ Pending transaction holding transaction "+transaction.identifier+ " NO MORE!!!"); + + let currentPendingTransactionPromise = this._saveChangesForTransaction(transaction); + return currentPendingTransactionPromise + .finally(() => { + if( this.pendingTransactionPromise === currentPendingTransactionPromise) { + this.pendingTransactionPromise = null; + } + }); + }) + } else { + console.debug("NO pending transaction holding transaction "+transaction.identifier); + this.pendingTransactionPromise = this._saveChangesForTransaction(transaction); + return this.pendingTransactionPromise; + } }, }, @@ -5359,15 +5446,19 @@ DataService.addClassProperties( promises; for (const iObjectDescriptor of transactionObjectDescriptors) { + let iComposedPath = iObjectDescriptor.composedPath; + + /* + We need to make aTransaction part of the composedPath. + */ + iComposedPath = iComposedPath.slice(); + iComposedPath.splice(iComposedPath.indexOf(this), 0, aTransaction, this); + let iPromise = this.dispatchDataEventTypeForObject( eventType, iObjectDescriptor, - { - transaction: aTransaction, - createdDataObjects: aTransaction.createdDataObjects.get(iObjectDescriptor), - updatedDataObjects: aTransaction.updatedDataObjects.get(iObjectDescriptor), - deletedDataObjects: aTransaction.deletedDataObjects.get(iObjectDescriptor) - } + aTransaction, + iComposedPath ); if(iPromise) { @@ -5403,9 +5494,10 @@ DataService.addClassProperties( value: function(transaction) { let self = this; - this.addPendingTransaction(transaction); let pendingTransactionPromise = new Promise(function (resolve, reject) { + self.addPendingTransaction(transaction); + try { var deletedDataObjectsIterator, operation, @@ -5435,6 +5527,7 @@ DataService.addClassProperties( operation.type = DataOperation.Type.NoOp; // console.log("saveChanges: transaction-"+this.identifier+" <- NoOp", transaction); resolve(operation); + self.removePendingTransaction(transaction); } self.dispatchWillSaveForTransaction(transaction) @@ -5487,7 +5580,9 @@ DataService.addClassProperties( validateFailedOperation.data = aggregatedValidationErrors; // Stop the transaction and resolve with the failure operation + self.removePendingTransaction(transaction); resolve(validateFailedOperation); + // self.pendingTransactionPromise = null; // TODO: @benoit should we not stop/reject the inner promise ? return null; @@ -5694,12 +5789,14 @@ DataService.addClassProperties( */ //console.log("saveChanges: done! transaction-"+this.identifier, transaction); - self.removePendingTransaction(transaction); + // self.removePendingTransaction(transaction); self.unregisterPendingTransactionPromise(transaction); self.dispatchDidSaveForTransaction(transaction) .then(() => { + self.removePendingTransaction(transaction); resolve(transaction); + // self.pendingTransactionPromise = null; }); }) .catch(function (error) { @@ -5814,15 +5911,20 @@ DataService.addClassProperties( transactionRollbackEvent.transaction = transaction; TransactionDescriptor.dispatchEvent(transactionRollbackEvent); + const self = this; return (transactionRollbackEvent.propagationPromise || Promise.resolve()) .then(function () { TransactionEvent.checkin(transactionRollbackEvent); return transaction.completionPromise; }) .then(function () { + // self.pendingTransactionPromise = null; + self.removePendingTransaction(transaction); rejectFunction(cancelError); }) .catch(function (error) { + // self.pendingTransactionPromise = null; + self.removePendingTransaction(transaction); rejectFunction(error); }) .finally(() => { @@ -9269,4 +9371,4 @@ Object.defineProperties(CollectionChanges.prototype, { } } -}) \ No newline at end of file +}) diff --git a/data/service/data-service.wip.js b/data/service/data-service.wip.js new file mode 100644 index 000000000..684ecabdd --- /dev/null +++ b/data/service/data-service.wip.js @@ -0,0 +1,9309 @@ +const Object = global.Object, //Cache for scope traversal performance + Montage = require("core/core").Montage, + Target = require("core/target").Target, + defaultEventManager = require("core/event/event-manager").defaultEventManager, + ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, + AuthorizationManager = require("./authorization-manager").defaultAuthorizationManager, + AuthorizationPolicy = require("./authorization-policy").AuthorizationPolicy, + AuthenticationPolicy = require("./authentication-policy").AuthenticationPolicy, + IdentityManager = require("./identity-manager").IdentityManager, + DataObjectDescriptor = require("../model/data-object-descriptor").DataObjectDescriptor, + Criteria = require("core/criteria").Criteria, + DataQuery = require("../model/data-query").DataQuery, + DataStream = require("./data-stream").DataStream, + DataTrigger = require("./data-trigger").DataTrigger, + Map = require("core/collections/map"), + Promise = require("core/promise").Promise, + Set = require("core/collections/set"), + CountedSet = require("core/counted-set").CountedSet, + ObjectPool = require("core/object-pool").ObjectPool, + DataEvent = require("../model/data-event").DataEvent, + PropertyDescriptor = require("core/meta/property-descriptor").PropertyDescriptor, + DeleteRule = require("core/meta/property-descriptor").DeleteRule, + deprecate = require("core/deprecate"), + currentEnvironment = require("core/environment").currentEnvironment, + PropertyChanges = require("core/collections/listen/property-changes"), + DataOperation = require("./data-operation").DataOperation, + Locale = require("core/locale").Locale, + ReadEvent = require("../model/read-event").ReadEvent, + DataOperationErrorNames = require("./data-operation").DataOperationErrorNames, + Transaction = require("../model/transaction").Transaction, + TransactionDescriptor = require("../model/transaction.mjson").montageObject, + TransactionEvent = require("../model/transaction-event").TransactionEvent, + ObjectStoreDescriptor = require("../model/object-store.mjson").montageObject, + ObjectPropertyStoreDescriptor = require("../model/object-property-store.mjson").montageObject + isArray = Array.isArray; + +require("core/extras/string"); +require("core/extras/date"); +require("core/extras/function"); + +var AuthorizationPolicyType = new Montage(); +AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; +AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRONT; +AuthorizationPolicyType.OnDemandAuthorizationPolicy = AuthorizationPolicy.ON_DEMAND; +AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON_FIRST_FETCH; + +AuthenticationPolicy.NoAuthenticationPolicy = AuthenticationPolicy.NONE; +AuthenticationPolicy.UpfrontAuthenticationPolicy = AuthenticationPolicy.UP_FRONT; +AuthenticationPolicy.OnDemandAuthenticationPolicy = AuthenticationPolicy.ON_DEMAND; +AuthenticationPolicy.OnFirstFetchAuthenticationPolicy = AuthenticationPolicy.ON_FIRST_FETCH; + +/** + * Future: Look at https://www.npmjs.com/package/broadcast-channel to implement cross-tab event distribution? + */ + +/** + * Provides data objects and manages changes to them. + * + * Data service subclasses that implement their own constructor should call this + * class' constructor at the beginning of their constructor implementation + * with code like the following: + * + * DataService.call(this); + * + * Currently only one service tree with one + * [root services]{@link DataService#rootService} is supported, and every + * instance of DataService or a DataService subclasses must either be that root + * service or be set as a descendent of that root service. + * + * + * @class + * @extends external:Montage + */ +const DataService = exports.DataService = class DataService extends Target { + /** @lends DataService */ + constructor() { + super(); + + this.defineBinding("mainService", { "<-": "mainService", source: defaultEventManager.application }); + this.defineBinding("types", { "<-": "childServices.map{types}", source: this }); + + // exports.DataService.mainService = exports.DataService.mainService || this; + // if(this === DataService.mainService) { + // // IdentityManager.mainService = DataService.mainService; + // //this.addOwnPropertyChangeListener("userLocales", this); + // this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); + // } + + //Deprecated now + //this._initializeAuthorization(); + + if (this.providesAuthorization) { + exports.DataService.authorizationManager.registerAuthorizationService(this); + } + + if (this.providesIdentity === true) { + IdentityManager.registerIdentityService(this); + } + + this._initializeOffline(); + + this._thenableByOperationId = new Map(); + //this._pendingOperationById = new Map(); + + // this._serializer = new MontageSerializer().initWithRequire(require); + // this._deserializer = new Deserializer(); + + //this.addOwnPropertyChangeListener("mainService", this); + } + + static { + Montage.defineProperties(this.prototype, { + // apiVersion: { + // value: "FROM AWS, NECESSARY FOR GCP?" + // } + date: { + get: function () { + return Date.date; + }, + }, + }); + } +}; + +// DataService = exports.DataService = Target.specialize(/** @lends DataService.prototype */ { + +// /*************************************************************************** +// * Initializing +// */ + +// constructor: { +// value: function DataService() { + +// this.defineBinding("mainService", {"<-": "mainService", source: defaultEventManager.application}); + +// // exports.DataService.mainService = exports.DataService.mainService || this; +// // if(this === DataService.mainService) { +// // // IdentityManager.mainService = DataService.mainService; +// // //this.addOwnPropertyChangeListener("userLocales", this); +// // this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); +// // } + +// //Deprecated now +// //this._initializeAuthorization(); + +// if (this.providesAuthorization) { +// exports.DataService.authorizationManager.registerAuthorizationService(this); +// } + +// if(this.providesIdentity === true) { +// IdentityManager.registerIdentityService(this); +// } + +// this._initializeOffline(); + +// this._thenableByOperationId = new Map(); +// //this._pendingOperationById = new Map(); + +// // this._serializer = new MontageSerializer().initWithRequire(require); +// // this._deserializer = new Deserializer(); + +// //this.addOwnPropertyChangeListener("mainService", this); +// } +// }, + +DataService.addClassProperties( + { + /** + * A reference to the application's main service. + * + * Applications typically have one and only one + * [root service]{@link DataService#rootService} to which all data requests + * are sent, and this is called the application's main service. That service + * can in turn delegate handling of different types of data to child + * services specialized by type. + * + * This property will be set automatically by bindings + * + * @type {DataService} + */ + mainService: { + get: function () { + return this._mainService; + }, + set: function (service) { + if (service !== this._mainService) { + this._mainService = service; + + if (this._mainService) { + this.addMainServiceEventListeners(); + } + } + }, + }, + + addMainServiceEventListeners: { + value: function () {}, + }, + + //Works with this.addOwnPropertyChangeListener("mainService", this) + handleMainServiceChange: { + value: function (mainService) { + //That only happens once + if (mainService) { + // mainService.addEventListener(DataOperation.Type.ReadOperation,this,false); + // mainService.addEventListener(DataOperation.Type.UpdateOperation,this,false); + // mainService.addEventListener(DataOperation.Type.CreateOperation,this,false); + // mainService.addEventListener(DataOperation.Type.DeleteOperation,this,false); + // mainService.addEventListener(DataOperation.Type.CreateTransactionOperation,this,false); + // mainService.addEventListener(DataOperation.Type.BatchOperation,this,false); + // mainService.addEventListener(DataOperation.Type.CommitTransactionOperation,this,false); + // mainService.addEventListener(DataOperation.Type.RollbackTransactionOperation,this,false); + + /* + Prepare to listen for ReadOperation that will be dispatched by RawDataServices that + handle that Read Event, so we can know the data services that handled it + */ + // self.addEventListener(DataOperation.Type.ReadOperation, self, true); + + mainService.addEventListener("change", this, false); + + mainService.addEventListener(DataOperation.Type.NoOp, this, false); + mainService.addEventListener(DataOperation.Type.ReadFailedOperation, this, false); + mainService.addEventListener(DataOperation.Type.ReadCompletedOperation, this, false); + mainService.addEventListener(DataOperation.Type.UpdateFailedOperation, this, false); + mainService.addEventListener(DataOperation.Type.UpdateCompletedOperation, this, false); + mainService.addEventListener(DataOperation.Type.CreateFailedOperation, this, false); + mainService.addEventListener(DataOperation.Type.CreateCompletedOperation, this, false); + mainService.addEventListener(DataOperation.Type.DeleteFailedOperation, this, false); + mainService.addEventListener(DataOperation.Type.DeleteCompletedOperation, this, false); + mainService.addEventListener(DataOperation.Type.CreateTransactionFailedOperation, this, false); + mainService.addEventListener(DataOperation.Type.CreateTransactionCompletedOperation, this, false); + + // mainService.addEventListener(DataOperation.Type.BatchCompletedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.BatchFailedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.TransactionUpdatedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.CommitTransactionFailedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.CommitTransactionCompletedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.RollbackTransactionFailedOperation,this,false); + // mainService.addEventListener(DataOperation.Type.RollbackTransactionCompletedOperation,this,false); + } + }, + }, + + application: { + get: function () { + return defaultEventManager.application; + }, + }, + + /*************************************************************************** + * Serialization + */ + + deserializeSelf: { + value: function (deserializer) { + var self = this, + result = this, + value; + + value = deserializer.getProperty("identifier"); + if (value) { + this._identifier = value; + } + + value = deserializer.getProperty("name"); + if (value) { + this.name = value; + } + + value = deserializer.getProperty("model") || deserializer.getProperty("binder"); + if (value) { + this.model = value; + } + + value = !this.model && deserializer.getProperty("types"); + if (value) { + var childServiceTypes = this._childServiceTypes; + childServiceTypes.push.apply(childServiceTypes, value); + } + + value = deserializer.getProperty("mappings"); + if (value) { + Array.prototype.push.apply(this._childServiceMappings, value); + } + + value = deserializer.getProperty("delegate"); + if (value) { + this.delegate = value; + } + + value = deserializer.getProperty("isUniquing"); + if (value !== undefined) { + this.isUniquing = value; + } + + value = deserializer.getProperty("childServices"); + if (value) { + this._deserializedChildServices = value; + } + + value = deserializer.getProperty("authorizationPolicy"); + if (value) { + this.authorizationPolicy = value; + } + + value = deserializer.getProperty("authenticationPolicy"); + if (value) { + this.authenticationPolicy = value; + } + + value = deserializer.getProperty("performsAccessControl"); + if (value) { + this.performsAccessControl = value; + } + + value = deserializer.getProperty("authorizedIdentiesNamedCriteria"); + if (value) { + this.authorizedIdenitiesNamedCriteria = value; + } + + value = deserializer.getProperty("accessPolicies"); + if (value) { + /* + accessPolicies are cumulative, but unfortunately we're missing an operator to add (nor remove) to a property that is an array with frb. We only have a replace + + TODO if we need AccessPolicy to have access to it's data service, we need to introduce a new addAccessPolicy() where we'll be able to add a policy.service = this. + */ + + this.accessPolicies.push.apply(this.accessPolicies, value); + } + + value = deserializer.getProperty("shouldAuthenticateReadOperation"); + if (value !== undefined) { + this.shouldAuthenticateReadOperation = value; + } + + value = deserializer.getProperty("originIdKey"); + if (value) { + this.originIdKey = value; + } + + value = deserializer.getProperty("autosaves"); + if (typeof value === "boolean") { + this.autosaves = value; + } + + return this; + }, + }, + + _deserializedChildServices: { + value: undefined, + }, + + deserializedFromSerialization: { + value: function (label) { + if (isArray(this._deserializedChildServices) && this._deserializedChildServices.length > 0) { + //var childServices = this._childServices; + // if(!this._childServices) { + // this._childServices = []; + // } + this.addChildServices(this._deserializedChildServices); + + //Sets ourselve as application's main service + if (!this.parentService && !defaultEventManager.application.mainService) { + defaultEventManager.application.service = + defaultEventManager.application.dataService = + defaultEventManager.application.mainService = + this; + } + } + + if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { + exports.DataService.authorizationManager.registerServiceWithUpfrontAuthorizationPolicy(this); + } + }, + }, + + currentEnvironment: { + value: currentEnvironment, + }, + + delegate: { + value: null, + }, + + /*************************************************************************** + * Basic Properties + * + * Private properties are defined where they are used, not here. + */ + + /** + * The types of data handled by this service. If this `undefined`, `null`, + * or an empty array this service is assumed to handled all types of data. + * + * The default implementation of this property returns the union of all + * types handled by child services of this service. Subclasses without child + * services should override this to directly return an array of the specific + * types they handle. + * + * Applications typically have one [raw data service]{@link RawDataService} + * service for each set of related data types and one + * [main service]{@link DataService.mainService} which is the parent of all + * those other services and delegates work to them based on the type of data + * to which the work applies. It is possible for child data services to have + * children of their own and delegate some or all of their work to them. + * + * A service's types must not be changed after it is added as a child of + * another service. + * + * @type {Array.} + */ + types: { + get: function () { + return this._childServiceTypes; + }, + set: function (value) { + if (value !== this._childServiceTypes) { + this._childServiceTypes = value; + } + }, + }, + + /** + * The data mappings used by this service to convert objects to raw + * data and vice-versa. + * + * @type {Array.} + */ + mappings: { + get: function () { + return this._childServiceMappings; + }, + }, + + /** + * A property that identifies the root DataService, from the outside. + * set by application, on both client and server side. + * + * It should be the same as rootService, which can only be known really when all DataServices + * and rawDataServices have been loaded, and is used from the perspectibe of child dataServices + * within their trees. + * + * @type {boolean} + */ + + _isMainService: { + value: false, + }, + isMainService: { + get: function () { + return this._isMainService; + }, + set: function (value) { + if (value !== this._isMainService) { + this._isMainService = value; + if (value) { + this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); + + /* + Prepare to listen for ReadOperation that will be dispatched by RawDataServices that + handle that Read Event, so we can know the data services that handled it + */ + this.application.addEventListener(DataOperation.Type.ReadOperation, this, true); + this.addEventListener("change", this, false); + + this.addEventListener(DataOperation.Type.NoOp, this, false); + this.addEventListener(DataOperation.Type.ReadFailedOperation, this, false); + this.addEventListener(DataOperation.Type.ReadCompletedOperation, this, false); + this.addEventListener(DataOperation.Type.UpdateFailedOperation, this, false); + this.addEventListener(DataOperation.Type.UpdateCompletedOperation, this, false); + this.addEventListener(DataOperation.Type.CreateFailedOperation, this, false); + this.addEventListener(DataOperation.Type.CreateCompletedOperation, this, false); + this.addEventListener(DataOperation.Type.DeleteFailedOperation, this, false); + this.addEventListener(DataOperation.Type.DeleteCompletedOperation, this, false); + this.addEventListener(DataOperation.Type.CreateTransactionFailedOperation, this, false); + this.addEventListener(DataOperation.Type.CreateTransactionCompletedOperation, this, false); + // this.addEventListener(DataOperation.Type.BatchCompletedOperation,this,false); + // this.addEventListener(DataOperation.Type.BatchFailedOperation,this,false); + // this.addEventListener(DataOperation.Type.TransactionUpdatedOperation,this,false); + // this.addEventListener(DataOperation.Type.CommitTransactionFailedOperation,this,false); + // this.addEventListener(DataOperation.Type.CommitTransactionCompletedOperation,this,false); + // this.addEventListener(DataOperation.Type.RollbackTransactionFailedOperation,this,false); + // this.addEventListener(DataOperation.Type.RollbackTransactionCompletedOperation,this,false); + } else { + this.removeRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); + } + } + }, + }, + + /*************************************************************************** + * Service Hierarchy + */ + + /** + * A read-only reference to the parent of this service. + * + * This value is modified by calls to + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} and cannot + * be modified directly. + * + * Data services that have no parents are called + * [root services]{@link DataService#rootService}. + * + * @type {?DataService} + */ + parentService: { + get: function () { + return this._parentService; + }, + }, + + /** + * Private settable parent service reference. + * + * This property's value should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {?DataService} + */ + _parentService: { + value: undefined, + }, + + /** + * A DataService's next target is it's parent + * @property {boolean} serializable + * @property {Component} value + */ + nextTarget: { + serializable: false, + get: function () { + return this._parentService; + }, + }, + + /** + * Convenience read-only reference to the root of the service tree + * containing this service. Most applications have only one root service, + * the application's [main service]{@link DataService.mainService}. + * + * @type {DataService} + */ + rootService: { + get: function () { + return DataService.mainService; + //return this.parentService ? this.parentService.rootService : this; + }, + }, + + /** + * Convenience method to assess if a dataService is the rootService + * + * @type {Boolean} + */ + isRootService: { + get: function () { + return this === this.rootService; + }, + }, + + /** + * The child services of this service. + * + * This value is modified by calls to + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} and must not + * be modified directly. + * + * @type {Set.} + */ + childServices: { + get: function () { + if (!this._childServices) { + this._childServices = new Set(); + } + return this._childServices; + }, + }, + + /** + * Private settable child service set. + * + * This property should not be modified outside of the + * [childServices getter]{@link DataService#childServices}, and its contents + * should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService} + * + * @private + * @type {?Set.} + */ + _childServices: { + value: undefined, + }, + + /** + * Adds child Services to the receiving service. + * + * @param {Array.} childServices. childServices to add. + */ + + addChildServices: { + value: function (childServices) { + var i, countI, iChild, j, countJ, mappings, jMapping, types; + + for (i = 0, countI = childServices.length; i < countI; i++) { + iChild = childServices[i]; + + if (this.childServices.has(iChild)) { + continue; + } + + if ((types = iChild.types)) { + this.registerTypes(types); + + // for(j=0, countJ = types.length;(j} + * @return {Promise} + */ + registerChildService: { + value: function (child, types) { + var self = this, + mappings = child.mappings || []; + // possible types + // -- types is passed in as an array or a single type. + // -- a model is set on the child. + // -- types is set on the child. + // any type can be asychronous or synchronous. + types = + (types && isArray(types) && types) || + (types && [types]) || + (child.model && child.model.objectDescriptors) || + (child.types && isArray(child.types) && child.types) || + (child.types && [child.types]) || + []; + + return child._childServiceRegistrationPromise.then(function () { + return self._registerChildServiceTypesAndMappings(child, types, mappings); + }); + }, + }, + + // #1 resolve asynchronous types + // -- types are arrays + // -- contents of the array can be: + // ---- an objectDescriptor or + // ---- a promise for an objectDescriptor or + // ---- a promise for an array of objectDescriptors + // -- flatten the result + // #2 map module id to object descriptor + // #3 register mapping to objectDescriptor + // -- resolve the mappings references + // -- map objectDescriptor to mapping + // #4 make prototype for object descriptor + // -- map constructor to prototype + // -- map objectDescriptor to prototype + // -- map objectDescriptor to dataTriggers + + // -- TODO: dataTriggers should be derived from all properties - mapping requisitePropertyNames + + _registerChildServiceTypesAndMappings: { + value: function (child, types, mappings) { + var self = this, + objectDescriptors; + return this.loadThenRegisterTypes(types) + .then(function (descriptors) { + objectDescriptors = descriptors; + return self._registerChildServiceMappings(child, mappings); + }) + .then(function () { + return self._makePrototypeForTypes(child, objectDescriptors); + }) + .then(function () { + self.addChildService(child, types); + return null; + }); + }, + }, + + loadThenRegisterTypes: { + value: function (types) { + return this._resolveAsynchronousTypes(types).then((objectDescriptors) => { + this.registerTypes(objectDescriptors); + return objectDescriptors; + }); + }, + }, + + _resolveAsynchronousTypes: { + value: function (types) { + var self = this; + return Promise.all( + this._flattenArray(types).map(function (type) { + return type; + }) + ).then(function (descriptors) { + return self._flattenArray(descriptors); + }); + }, + }, + + _flattenArray: { + value: function (array) { + return Array.prototype.concat.apply([], array); + }, + }, + + _registerObjectDescriptor: { + value: function (jObjectDescriptor, moduleIdToObjectDescriptorMap = this._moduleIdToObjectDescriptorMap) { + var result = null; + + if (jObjectDescriptor.object) { + this._constructorToObjectDescriptorMap.set(jObjectDescriptor.object, jObjectDescriptor); + } else if (jObjectDescriptor.module && typeof jObjectDescriptor.loadObjectFromModule === "function") { + var self = this; + result = jObjectDescriptor.loadObjectFromModule().then(function () { + self._constructorToObjectDescriptorMap.set(jObjectDescriptor.object, jObjectDescriptor); + }); + } + var jModule = jObjectDescriptor.module; + if (!jModule) { + jModuleId = Montage.getInfoForObject(this).moduleId; + } else { + jModuleId = jModule.id; + jModuleId += "/"; + jModuleId += jObjectDescriptor.exportName; + } + moduleIdToObjectDescriptorMap[jModuleId] = jObjectDescriptor; + + //Setup the event propagation chain + /* + this is now done in objectDescriptor as it follows the hierachy of objectDescriptor before getting to DataServices. + */ + // jObjectDescriptor.nextTarget = service; + return result; + }, + }, + + registerTypes: { + value: function (types) { + var map = this._moduleIdToObjectDescriptorMap, + typesPromises, + j, + countJ, + jObjectDescriptor, + jResult, + self = this; + + for (j = 0, countJ = types.length; j < countJ; j++) { + jObjectDescriptor = types[j]; + + jResult = this._registerObjectDescriptor(jObjectDescriptor, map); + + // jResult = this._makePrototypeForType(service, jObjectDescriptor); + if (jResult && Promise.is(jResult)) { + (typesPromises || (typesPromises = [])).push(jResult); + } + } + + // types.forEach(function (objectDescriptor) { + // var module = objectDescriptor.module, + // moduleId = [module.id, objectDescriptor.exportName].join("/"); + // map[moduleId] = objectDescriptor; + // }); + + if (typesPromises) { + this._childServiceRegistrationPromise = Promise.all(typesPromises); + } + }, + }, + + _registerChildServiceMappings: { + value: function (child, mappings) { + var self = this; + return Promise.all( + mappings.map(function (mapping) { + return self._addMappingToChild(mapping, child); + }) + ); + }, + }, + + _makePrototypeForTypes: { + value: function (childService, types) { + var self = this; + return Promise.all( + types.map(function (objectDescriptor) { + return self._makePrototypeForType(childService, objectDescriptor); + }) + ); + }, + }, + + _makePrototypeForType: { + value: function (childService, objectDescriptor) { + if (objectDescriptor.object) { + return this.__makePrototypeForType(childService, objectDescriptor, objectDescriptor.object); + } else { + var self = this, + module = objectDescriptor.module; + if (module && typeof objectDescriptor.loadObjectFromModule === "function") { + return objectDescriptor.loadObjectFromModule().then(function (moduleObject) { + return self.__makePrototypeForType(childService, objectDescriptor, moduleObject); + }); + + // return module.require.async(module.id).then(function (exports) { + // return self.__makePrototypeForType(childService, objectDescriptor, exports[objectDescriptor.exportName]); + // }); + } else { + return Promise.resolveNull; + } + } + }, + }, + + __makePrototypeForType: { + value: function (childService, objectDescriptor, constructor) { + /* to handle things like native types passing by */ + if (!childService) { + return constructor + ? constructor.prototype + : objectDescriptor?.object + ? objectDescriptor.object.prototype + : typeof objectDescriptor === "function" + ? objectDescriptor + : null; + } + + var prototype = Object.create( + constructor?.prototype + ? constructor.prototype + : typeof objectDescriptor === "function" + ? objectDescriptor + : Object + ), + mapping = childService.mappingForType(objectDescriptor), + /* + FIXME + we're "lucky" here as when this is called, the current DataService hasn't registered yet the mappings, so we end up creating triggers for all property descriptors. + */ + requisitePropertyNames = (mapping && mapping.requisitePropertyNames) || new Set(), + dataTriggers = this.DataTrigger.addTriggers( + this, + objectDescriptor, + prototype, + requisitePropertyNames + ), + mainService = this.rootService; + + Object.defineProperty(prototype, "dataIdentifier", { + enumerable: true, + get: function () { + return mainService.dataIdentifierForObject(this); + }, + }); + Object.defineProperty(prototype, "snapshot", { + enumerable: true, + get: function () { + /* + this is making a big assumption that there's only one raw data service handling this, + but the whole point of introducing data operations was to open up the fact that there could be multiple, + like a cloud + a local. + + So we might want to keep an eye on this, even though all should be in-sync if they handle the same properties. + */ + return this.dataIdentifier.dataService.snapshotForObject(this); + // return mainService._getChildServiceForObject(this)?.snapshotForObject(this); + }, + }); + Object.defineProperty(prototype, "nextTarget", { + enumerable: true, + get: function () { + return this.objectDescriptor; + }, + }); + + /* + OPTIMIZE ME: We need to be smarter and only do that for the highest levels as it will be inherited + */ + Object.defineProperty(prototype, "propertyChanges_prototype_addOwnPropertyChangeListener", { + value: this.propertyChanges_prototype_addOwnPropertyChangeListener, + }); + Object.defineProperty(prototype, "addOwnPropertyChangeListener", { + value: this._dataObject_addOwnPropertyChangeListener, + }); + + Object.defineProperty(prototype, "propertyChanges_prototype_removeOwnPropertyChangeListener", { + value: this.propertyChanges_prototype_removeOwnPropertyChangeListener, + }); + Object.defineProperty(prototype, "removeOwnPropertyChangeListener", { + value: this._dataObject_removeOwnPropertyChangeListener, + }); + + this._dataObjectPrototypes.set(constructor, prototype); + this._dataObjectPrototypes.set(objectDescriptor, prototype); + this._dataObjectTriggers.set(objectDescriptor, dataTriggers); + // this._constructorToObjectDescriptorMap.set(constructor, objectDescriptor); + return prototype; + }, + }, + + _addMappingToChild: { + value: function (mapping, child) { + var service = this; + return Promise.all([mapping.objectDescriptor, mapping.rawDataDescriptor]).then(function ([ + objectDescriptor, + rawDataDescriptor, + ]) { + // TODO -- remove looking up by string to unique. + var type = [objectDescriptor.module.id, objectDescriptor.name].join("/"); + objectDescriptor = service._moduleIdToObjectDescriptorMap[type]; + mapping.objectDescriptor = objectDescriptor; + mapping.rawDataDescriptor = rawDataDescriptor; + mapping.service = child; + child.addMappingForType(mapping, objectDescriptor); + return null; + }); + }, + }, + + objectDescriptorForType: { + value: function (type) { + var descriptor = + this._constructorToObjectDescriptorMap.get(type) || + (typeof type === "string" && this._moduleIdToObjectDescriptorMap[type]); + + return descriptor || type; + }, + }, + + _constructorToObjectDescriptorMap: { + value: new Map(), + }, + + _moduleIdToObjectDescriptorMap: { + value: {}, + }, + + /** + * Remove a raw data service as a child of this service and clear its parent + * if that service is a child of this service. + * + * The performance of this method is O(m) + O(n), where m is the number of + * children of this service handling the same type as the child service to + * remove and n is the number of types handled by all children of this + * service. + * + * @method + * @argument {RawDataService} service + * @argument {Array} [types] Types to use instead of the child's types. + */ + removeChildService: { + value: function (child, types) { + var type, chidren, index, i, n; + types = types || child.types; + // Remove the child service from the services array of each of its + // types or from the "all types" service array identified by the + // `null` type, or remove a type altogether if its service array + // only contains the child service to remove, or remove the "all + // types" service array if it only contains the child service to + // remove. + for (i = 0, n = (types && types.length) || 1; i < n; i += 1) { + type = (types && types.length && types[i]) || null; + chidren = this._childServicesByType.get(type); + index = chidren ? chidren.indexOf(child) : -1; + if (index >= 0 && chidren.length > 1) { + chidren.splice(index, 1); + } else if (index === 0) { + this._childServicesByType.delete(type); + index = type ? this._childServiceTypes.indexOf(type) : -1; + if (index >= 0) { + this._childServiceTypes.splice(index, 1); + } + } + } + // Remove the child from this service's children set. + this.childServices.delete(child); + // Clear the service parent if appropriate. + if (child._parentService === this) { + child._parentService = undefined; + } + }, + }, + + /** + * Alternative to [removeChildService()]{@link DataService#removeChildService}. + * While removeChildService is synchronous, unregisterChildService is asynchronous + * and may take a child whose [types]{@link DataService#types} property is + * a promise instead of an array. + * + * This is useful for example if the child service does not know its types + * immediately, e.g. if it must fetch them from a .mjson descriptors file. + * + * If the child's types is an array, it is guaranteed to behave exactly + * like removeChildService. + * + * @method + * @return {Promise} + */ + unregisterChildService: { + value: function (child) { + var self = this; + return new Promise(function (resolve, reject) { + self.removeChildService(child, child.types); + resolve(); + }); + }, + }, + + /** + * A map from each of the data types handled by this service to an array + * of the child services that can handle that type, with each such array + * ordered according to the order in which the services in it were + * [added]{@link DataService#addChildService} as children of this service. + * + * If one or more child services of this service are defined as handling all + * types (their [types]{@link DataService#types} property is `undefined`, + * `null`, or an empty array), the child service map also include a `null` + * key whose corresponding value is an array of all those services defined + * to handle all types. + * + * The contents of this map should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {Map>} + */ + _childServicesByType: { + get: function () { + if (!this.__childServicesByType) { + this.__childServicesByType = new Map(); + } + return this.__childServicesByType; + }, + }, + + __childServicesByType: { + value: undefined, + }, + + /** + * Returns true if type is one of the types a data service has been configured to support. + * Which means at the minimum how to map a type from raw data.x + * + * Note that to have a mmore fine-grained assessment of what a DataService can do with that type, + * Use handlesDataOperationForType() / handlesDataOperationTypeForType() + * + * @method + * @argument {ObjectDescriptor} type + * @return {boolean} + */ + handlesType: { + value: function (type) { + return ( + this.types.indexOf(type) !== -1 + /* || (this.childServicesForType(type)?.length > 0)*/ + ); + //return (this.rootService._childServicesByType.get(type).indexOf(this) !== -1); + }, + }, + + /** + * Returns true if the data service knows how to handle the passed dataOperation + * + * Note that to have a mmore fine-grained assessment of what a DataService can do with that type, + * Use handlesDataOperationForType() / handlesDataOperationTypeForType() + * + * @method + * @argument {DataOperation} dataOperation + * @argument {ObjectDescriptor} type + * @return {boolean} + */ + handlesDataOperation: { + value: function (dataOperation) { + return this.handlesDataOperationTypeForType(dataOperation.type, dataOperation.target); + }, + }, + + /** + * Returns true if the data service knows how to handle the passed dataOperation's type + * for the passed type / objectDescriptor + * + * By default it assumes a DataService handles all types of operation per data type + * + * Would it be better named: + * - handlesDataOperationTypeForDataType ? + * - handlesDataOperationTypeForObjectDescriptor ? + * + * @method + * @argument {DataOperation} dataOperation + * @argument {ObjectDescriptor} type + * @return {boolean} + */ + handlesDataOperationTypeForType: { + value: function (dataOperationType, type) { + return this.handlesType(type); + }, + }, + + _childServicesByIdentifier: { + get: function () { + if (!this.__childServicesByIdentifier) { + this.__childServicesByIdentifier = new Map(); + } + return this.__childServicesByIdentifier; + }, + }, + + __childServicesByIdentifier: { + value: undefined, + }, + + /** + * An array of the data types handled by all child services of this service. + * + * The contents of this map should not be modified outside of + * [addChildService()]{@link DataService#addChildService} and + * [removeChildService()]{@link DataService#removeChildService}. + * + * @private + * @type {Array.} + */ + _childServiceTypes: { + get: function () { + if (!this.__childServiceTypes) { + this.__childServiceTypes = []; + } + return this.__childServiceTypes; + }, + }, + + __childServiceTypes: { + value: undefined, + }, + + /** + * Get the first child service that can handle the specified object, + * or `null` if no such child service exists. + * + * @private + * @method + * @argument {Object} object + * @returns DataService + */ + _getChildServiceForObject: { + value: function (object) { + return this.childServiceForType(this.rootService._getObjectType(object)); + }, + }, + + /** + * Get all child services that handle data of the specified type argument, + * or `null` if no such child service exists. + * + * @method + * @argument {ObjectDescriptor} type + * @returns {Array} + */ + childServicesForType: { + value: function (type) { + var services; + type = type instanceof ObjectDescriptor ? type : this.objectDescriptorForType(type); + //services = this._childServicesByType.get(type) || this._childServicesByType.get(null); + services = this._childServicesByType.get(type); + /* + Fixing bug were a catch-all data service, one that states it handles all by having no type + could end up multiple times in services as the following block was executed at every call with the same type argument. + + So freezing the array storing the results in this._childServicesByType allows us to know we + don't need to do it again + */ + if (services) { + let catchAllServices = this._childServicesByType.get(null); + if (catchAllServices && !Object.isFrozen(services)) { + services.push(...catchAllServices); + Object.freeze(services); + } + } else { + services = this._childServicesByType.get(null); + } + return services || null; + }, + }, + + /** + * Get all child services that handle a dataOperation, which can mean: + * - handling the type of operation — dataOperation.type + * - handling the object descriptor involved - dataOperation.target + * - but also handling the readExpressions involved — dataOperation.data.readExpresssions + * + * Just because a RawDataService can read objects of a certain type, doesn't necessarily means + * it can resolves a property/relationship on the source object descriptor (dataOperation.target) to that type + * + * TODO: ADD CACHING + * + * @method + * @argument {DataOperationType} dataOperationtype + * @argument {ObjectDescriptor} type + * @returns {Array} + */ + childServicesHandlingDataOperation: { + value: function (dataOperation) { + return this.childServicesHandlingDataOperationTypeForType(dataOperation.type, dataOperation.target); + }, + }, + + /** + * Get all child services that handle dataOperationtype of the specified type argument, + * or `null` if no such child service exists. + * + * TODO: ADD CACHING + * + * @method + * @argument {DataOperationType} dataOperationtype + * @argument {ObjectDescriptor} type + * @returns {Array} + */ + childServicesHandlingDataOperationTypeForType: { + value: function (dataOperationtype, type) { + return this.childServicesForType(type)?.filter( + (service) => service.handlesDataOperationTypeForType(dataOperationtype, type) === true + ); + }, + }, + + /* + TODO: cache / optimize + */ + childServicesThatCanSaveDataType: { + value: function (type) { + return this.childServicesForType(type)?.filter((service) => service.canSaveData === true); + }, + }, + + /** + * Get all descendant child services that can handle data of the specified type, + * or `null` if no such child service exists. They are ordered to be depth first, followed by their parent + * + * @private + * @method + * @argument {DataObjectDescriptor} type + * @returns {Set.} + */ + + __descendantServicesByType: { + value: undefined, + }, + _descendantServicesByType: { + get: function () { + if (!this.__descendantServicesByType) { + this.__descendantServicesByType = new Map(); + } + return this.__descendantServicesByType; + }, + }, + + descendantServicesForType: { + value: function (type) { + var descendantServicesForType = this._descendantServicesByType.get(type); + + if (!descendantServicesForType) { + var childServices = this.childServicesForType(type), + countI = childServices?.length || 0; + + if (countI > 0) { + var i = 0, + iChildService, + iDescendantServicesForType; + + descendantServicesForType = []; + + do { + iChildService = childServices[i]; + + if (iChildService) { + iDescendantServicesForType = iChildService.descendantServicesForType(type); + if (iDescendantServicesForType) { + descendantServicesForType.push.apply( + descendantServicesForType, + iDescendantServicesForType + ); + } + descendantServicesForType.push(iChildService); + } + i++; + } while (i < countI); + } else if (this.handlesType(type)) { + descendantServicesForType = this; + } else { + descendantServicesForType = null; + } + + this._descendantServicesByType.set(type, descendantServicesForType); + } + + return descendantServicesForType; + }, + }, + + /** + * Get the first child service that can handle data of the specified type, + * or `null` if no such child service exists. + * + * @private + * @method + * @argument {DataObjectDescriptor} type + * @returns {Set.} + */ + childServiceForType: { + value: function (type) { + var services = this.childServicesForType(type); + return (services && services[0]) || null; + }, + }, + + /*************************************************************************** + * Mappings + */ + + /** + * Adds a mapping to the service for the specified + * type. + * @param {DataMapping} mapping. The mapping to use. + * @param {ObjectDescriptor} type. The object type. + */ + addMappingForType: { + value: function (mapping, type) { + mapping.service = mapping.service || this; + this._mappingByType.set(type, mapping); + }, + }, + + /** + * Return the mapping to use for the specified type. + * @param {ObjectDescriptor} type. + * @returns {DataMapping|null} returns the specified mapping or null + * if a mapping is not defined for the specified type. + * + * If an immediate mapping isn't found, we look up the parent chain + */ + mappingForType: { + value: function (type) { + if (this.isRootService) { + var childService = this.childServiceForType(type); + if (childService) { + return childService.mappingForType(type); + } else { + return null; + } + } else { + var mapping, + localType = this.objectDescriptorForType(type); + + while ( + localType && + !(mapping = this._mappingByType.has(localType) && this._mappingByType.get(localType)) + ) { + localType = localType.parent; + } + return mapping || null; + } + }, + }, + + mappingWithType: { + value: deprecate.deprecateMethod( + void 0, + function (type) { + return this.mappingForType(type); + }, + "mappingWithType", + "mappingForType" + ), + }, + + _mappingByType: { + get: function () { + if (!this.__mappingByType) { + this.__mappingByType = new Map(); + } + return this.__mappingByType; + }, + }, + + __mappingByType: { + value: undefined, + }, + + _childServiceMappings: { + get: function () { + if (!this.__childServiceMappings) { + this.__childServiceMappings = []; + } + return this.__childServiceMappings; + }, + }, + + __childServiceMappings: { + value: undefined, + }, + + /*************************************************************************** + * Models + */ + + /** + * The [model]{@link ObjectModel} that this service supports. If the model is + * defined the service supports all the object descriptors contained within the model. + */ + _model: { + value: undefined, + }, + + model: { + get: function () { + return this._model; + }, + set: function (value) { + if (value !== this._model) { + var childServiceTypes = this._childServiceTypes; + //TODO Remove correctly the types of the model going away: handling of events, etc... + this._model = value; + childServiceTypes.push.apply(childServiceTypes, this._model.objectDescriptors); + } + }, + }, + + /** + * The maximum amount of time a DataService's data will be considered fresh. + * ObjectDescriptor's maxAge should take precedence over this and a DataStream's dataMaxAge should + * take precedence over a DataService's dataMaxAge global default value. + * + * @type {Number} + */ + dataMaxAge: { + value: undefined, + }, + + /*************************************************************************** + * + * Authorization + * + ***************************************************************************/ + + _initializeAuthorization: { + value: function () { + if (this.providesAuthorization) { + exports.DataService.authorizationManager.registerAuthorizationService(this); + } + + if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { + var self = this; + this.authorizationPromise = exports.DataService.authorizationManager + .authorizeService(this) + .then(function (authorization) { + self.authorization = authorization; + return authorization; + }); + } else { + //Service doesn't need anything upfront, so we just go through + this.authorizationPromise = Promise.resolve(); + } + }, + }, + + /** + * Returns the AuthorizationPolicyType used by this DataService. + * + * @type {AuthorizationPolicyType} + */ + authorizationPolicy: { + value: AuthorizationPolicyType.NoAuthorizationPolicy, + }, + + /** + * holds authorization object after a successful authorization + * + * @type {Object} + */ + + authorization: { + value: undefined, + }, + + authorizationPromise: { + value: Promise.resolve(), + }, + + /** + * Returns the list of moduleIds of DataServices a service accepts to provide + * authorization on its behalf. If an array has multiple + * authorizationServices, the final choice will be up to the App user + * regarding which one to use. This array is expected to return moduleIds, + * not objects, allowing the AuthorizationManager to manage unicity + * + * @type {string[]} + */ + authorizationServices: { + value: null, + }, + + /** + * @type {string} + * @description Module ID of the panel component used to gather necessary authorization information + */ + authorizationPanel: { + value: undefined, + }, + + /** + * Indicates whether a service can provide user-level authorization to its + * data. Defaults to false. Concrete services need to override this as + * needed. + * + * @type {boolean} + */ + providesAuthorization: { + value: false, + }, + + /** + * Performs whatever tasks are necessary to authorize + * this service and returns a Promise that resolves with + * an Authorization object. + * + * @method + * @returns Promise + */ + authorize: { + value: undefined, + }, + + /********* New set of methods for user identity and authentication **********/ + + /** + * Indicates whether a service can provide application user-identity . + * Defaults to false. Concrete services need to override this as + * needed. + * + * @type {boolean} + */ + + providesIdentity: { + value: false, + }, + + /** + * Returns the AuthenticationPolicyType used by a DataService. For enabling both + * system to co-exists for upgradability, there is no default here. + * DataServices suclass have to provide it. + * + * @type {AuthorizationPolicyType} + */ + authenticationPolicy: { + value: AuthorizationPolicyType.NoAuthorizationPolicy, + }, + + /** + * The identity for the data service. It could be an unauthenticated/ + * anonymous identity, a user identity or an agent/application identity + * + * @type {Object} + */ + + identity: { + value: undefined, + }, + + /** + * a promise to the user identity for the data service. This is necessary to buffer + * fetch/data operations that can't be executed until a valid user identity is known. + * + * @type {Object} + */ + + identityPromise: { + value: Promise.resolve(), + }, + + /** + * The list of DataServices a service accepts to provide + * authorization on its behalf. If an array has multiple + * authorizationServices, the final choice will be up to the App user + * regarding which one to use. This array is expected to return moduleIds, + * not objects, allowing the AuthorizationManager to manage unicity + * + * @type {string[]} + */ + identityServices: { + value: null, + }, + + /** + * + * @method + * @returns Promise + */ + logOut: { + value: undefined, + }, + + /*************************************************************************** + * Data Object Types + */ + + /** + * Returns an object descriptor for the provided object. If this service + * does not have an object descriptor for this object it will ask its + * parent for one. + * + * TODO: looks like we're looping all the time and not caching a lookup" + * Why isn't objectDescriptorWithModuleId used?? + * + * @param {object} + * @returns {ObjectDescriptor|null} if an object descriptor is not found this + * method will return null. + */ + objectDescriptorForObject: { + value: function (object) { + var objectDescriptor = this._objectDescriptorForObjectCache.get(object); + + if (!objectDescriptor) { + objectDescriptor = this.objectDescriptorForType(object?.constructor); + } + + if (!objectDescriptor) { + var types = this.types, + objectInfo = Montage.getInfoForObject(object), + moduleId = objectInfo.moduleId, + objectName = objectInfo.objectName, + module, + exportName, + i, + n; + + objectDescriptor = this.objectDescriptorWithModuleId(moduleId); + for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { + module = types[i].module; + exportName = module && types[i].exportName; + if (module && moduleId === module.id && objectName === exportName) { + if (objectDescriptor !== types[i]) { + console.error( + "objectDescriptorWithModuleId cached an objectDescriptor and objectDescriptorForObject finds another" + ); + } + objectDescriptor = types[i]; + } + } + return ( + objectDescriptor || (this.parentService && this.parentService.objectDescriptorForObject(object)) + ); + } else { + return objectDescriptor; + } + }, + }, + + _objectDescriptorByModuleId: { + value: undefined, + }, + _mjsonExtension: { + value: ".mjson", + }, + objectDescriptorWithModuleId: { + value: function (objectDescriptorModuleId) { + if (!this._objectDescriptorByModuleId) { + var map = (this._objectDescriptorByModuleId = new Map()); + + var types = this.types, + i, + n, + iType, + iInfo, + iModuleId, + mjsonExtension = this._mjsonExtension; + for (i = 0, n = types.length; i < n; i++) { + iType = types[i]; + if (!iType.module) { + iInfo = Montage.getInfoForObject(iType); + iModuleId = iInfo.moduleId; + if (iModuleId.endsWith(mjsonExtension)) { + iModuleId = iModuleId.removeSuffix(mjsonExtension); + } + map.set(iModuleId, iType); + } else { + map.set(iType.module.id, iType); + } + } + } + return this._objectDescriptorByModuleId.get(objectDescriptorModuleId); + }, + }, + + /** + * Get the type of the specified data object. + * + * @private + * @method + * @argument {Object} object - The object whose type is sought. + * @returns {DataObjectDescriptor} - The type of the object, or undefined if + * no type can be determined. + */ + _getObjectType: { + value: function (object) { + var type = this._typeRegistry.get(object), + moduleId = typeof object === "string" ? object : this._getModuleIdForObject(object); + while (!type && object) { + if (object.constructor.TYPE instanceof DataObjectDescriptor) { + type = object.constructor.TYPE; + } else if (this._moduleIdToObjectDescriptorMap[moduleId]) { + type = this._moduleIdToObjectDescriptorMap[moduleId]; + } else { + object = Object.getPrototypeOf(object); + } + } + return type; + }, + }, + + _getModuleIdForObject: { + value: function (object) { + var info = Montage.getInfoForObject(object); + return [info.moduleId, info.objectName].join("/"); + }, + }, + + /** + * Register the type of the specified data object if necessary. + * + * @private + * @method + * @argument {Object} object + * @argument {DataObjectDescriptor} type + */ + _setObjectType: { + value: function (object, type) { + if (this._getObjectType(object) !== type) { + this._typeRegistry.set(object, type); + } + }, + }, + + _typeRegistry: { + get: function () { + if (!this.__typeRegistry) { + this.__typeRegistry = new WeakMap(); + } + return this.__typeRegistry; + }, + }, + + shouldListenForRemoteObjectPropertyChange: { + value: function (dataObjecy, key, beforeChange) { + return false; + }, + }, + + propertyChanges_prototype_addOwnPropertyChangeListener: { + value: PropertyChanges.prototype.addOwnPropertyChangeListener, + }, + propertyChanges_prototype_removeOwnPropertyChangeListener: { + value: PropertyChanges.prototype.removeOwnPropertyChangeListener, + }, + + /* + This is executed on DataObject instances, so this is an instance of a DataObject. + */ + __dataObject_addOwnPropertyChangeListener: { + value: undefined, + }, + + _dataObject_addOwnPropertyChangeListener: { + get: function () { + if (!this.__dataObject_addOwnPropertyChangeListener) { + var dataService = this; + + this.__dataObject_addOwnPropertyChangeListener = function ( + key, + listener, + beforeChange, + trackRemoteChanges + ) { + if ( + trackRemoteChanges || + dataService.shouldListenForRemoteObjectPropertyChange(this, key, beforeChange) + ) { + // if(dataService.shouldAddEventListenerForObjectRemotePropertyChange(this,key,beforeChange)) { + dataService.trackRemoteObjectPropertyChanges(this, key); + } + return this.propertyChanges_prototype_addOwnPropertyChangeListener(key, listener, beforeChange); + }; + } + return this.__dataObject_addOwnPropertyChangeListener; + }, + }, + + __dataObject_removeOwnPropertyChangeListener: { + value: undefined, + }, + _dataObject_removeOwnPropertyChangeListener: { + get: function () { + if (!this.__dataObject_removeOwnPropertyChangeListener) { + var dataService = this; + + this.__dataObject_removeOwnPropertyChangeListener = function ( + key, + listener, + beforeChange, + trackRemoteChanges + ) { + if ( + trackRemoteChanges || + dataService.shouldListenForRemoteObjectPropertyChange(this, key, beforeChange) + ) { + // if(dataService.shouldAddEventListenerForObjectRemotePropertyChange(this,key,beforeChange)) { + dataService.removeEventListener(""); + } + + return this.propertyChanges_prototype_removeOwnPropertyChangeListener( + key, + listener, + beforeChange + ); + }; + } + return this.__dataObject_removeOwnPropertyChangeListener; + }, + }, + + /*************************************************************************** + * Data Object Triggers + */ + + /** + * Returns a prototype for objects of the specified type. The returned + * prototype will have a [data trigger]{@link DataTrigger} defined for each + * lazy relationships and properties of that type. A single prototype will + * be created for all objects of a given type. + * + * @private + * @method + * @argument {DataObjectDescriptor|ObjectDescriptor} type + * @returns {Object} + */ + _getPrototypeForType: { + value: function (type, _rawDataService) { + var info, triggers, prototypeToExtend, prototype; + type = this.objectDescriptorForType(type); + prototype = this._dataObjectPrototypes.get(type); + if (type && !prototype) { + return this.__makePrototypeForType( + _rawDataService || this.childServiceForType(type), + type, + type.object + ); + + // //type.objectPrototype is legacy and should be depreated over time + // prototypeToExtend = type.objectPrototype || Object.getPrototypeOf(type.module) || Montage.prototype; + // prototype = Object.create(prototypeToExtend); + // prototype.constuctor = type.objectPrototype ? type.objectPrototype.constructor + // : type.module; + + // if(prototype.constuctor.name === "constructor" ) { + // Object.defineProperty(prototype.constuctor, "name", { value: type.typeName }); + // } + + // this._dataObjectPrototypes.set(type, prototype); + // if (type instanceof ObjectDescriptor || type instanceof DataObjectDescriptor) { + // triggers = this.DataTrigger.addTriggers(this, type, prototype); + // } else { + // info = Montage.getInfoForObject(type.prototype); + // console.warn("Data Triggers cannot be created for this type. (" + (info && info.objectName) + ") is not an ObjectDescriptor"); + // triggers = []; + // } + // this._dataObjectTriggers.set(type, triggers); + // //We add a property that returns an object's snapshot + // //We add a property that returns an object's primaryKey + // //Let's postponed this for now and revisit when we need + // //add more properties/logic to automatically track changes + // //on objects + + // // Object.defineProperties(prototype, { + // // "montageDataSnapshot": { + // // get: this.__object__snapshotMethodImplementation + // // }, + // // "montageDataPrimaryKey": { + // // get: this.__object_primaryKeyMethodImplementation + // // } + // // }); + + // //Adds support for event structure, setting the classes as instances next target + // //if there's type.module + // if(type.module) { + // Object.defineProperty(prototype, "nextTarget", { value: type.module }); + + // //setting objectDescriptor as classes next target: + // Object.defineProperty(type.module, "nextTarget", { value: type }); + // } else { + // //If no known custom JS constructor, we go straight to the object descriptor: + // Object.defineProperty(prototype, "nextTarget", { value: type }); + // } + + // // //set data service as objectDescriptor next target: + // // Object.defineProperty(type, "nextTarget", { value: this.mainService }); + } + return prototype; + }, + }, + + // __object__snapshotMethodImplementation: { + // value: function() { + // return exports.DataService.mainService._getChildServiceForObject(this).snapshotForObject(this); + // } + // }, + // __object_primaryKeyMethodImplementation: { + // value: function() { + // return exports.DataService.mainService.dataIdentifierForObject(this).primaryKey; + // } + // }, + + /** + * Returns the [data triggers]{@link DataTrigger} set up for objects of the + * specified type. + * + * @private + * @method + * @argument {Object} object + * @returns {Object} + */ + _getTriggersForObject: { + value: function (object) { + var type = this._getObjectType(object), + triggers = type && this._dataObjectTriggers.get(type); + if (!triggers && this.handlesType(type)) { + } + return triggers; + }, + }, + + _triggerForObjectProperty: { + value: function (object, propertyName) { + return this._getTriggersForObject(object)?.[propertyName]; + }, + }, + + _dataObjectPrototypes: { + // get: function () { + // if (!this.__dataObjectPrototypes){ + // this.__dataObjectPrototypes = new Map(); + // } + // return this.__dataObjectPrototypes; + // }, + value: new Map(), + }, + + __dataObjectPrototypes: { + value: undefined, + }, + + _dataObjectTriggers: { + get: function () { + if (!this.__dataObjectTriggers) { + this.__dataObjectTriggers = new Map(); + } + return this.__dataObjectTriggers; + }, + }, + + __dataObjectTriggers: { + value: undefined, + }, + + /*************************************************************************** + * Data Object Properties + */ + + /** + * Since root services are responsible for triggering data objects fetches, + * subclasses whose instances will not be root services should override this + * method to call their root service's implementation of it. + * + * @todo Rename and document API and implementation. + * + * @method + */ + decacheObjectProperties: { + value: function (object, propertyNames) { + if (this.isRootService) { + var names = isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1, + triggers = this._getTriggersForObject(object), + trigger, + i, + n; + for (i = start, n = names.length; i < n; i += 1) { + trigger = triggers && triggers[names[i]]; + if (trigger) { + trigger.decacheObjectProperty(object); + } + } + } else { + this.rootService.decacheObjectProperties(object, propertyNames); + } + }, + }, + + /** + * Request possibly asynchronous values of a data object's properties. These + * values will only be fetched if necessary and only the first time they are + * requested. + * + * To force an update of a value that was previously obtained or set, use + * [updateObjectProperties()]{@link DataService#updateObjectProperties} + * instead of this method. + * + * Since root services are responsible for determining when to fetch or + * update data objects values, subclasses whose instances will not be root + * services should override this method to call their root service's + * implementation of it. + * + * Subclasses should define how property values are obtained by overriding + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead + * of this method. That method will be called by this method when needed. + * + * Although this method returns a promise, the requested data will not be + * passed in to the promise's callback. Instead that callback will received + * a `null` value and the requested values will be set on the specified + * properties of the object passed in. Those values can be accessed there + * when the returned promise is fulfilled, as in the following code: + * + * myService.getObjectProperties(myObject, "x", "y").then(function () { + * someFunction(myObject.x, myObject.y); + * } + * + * @method + * @argument {object} object - The object whose property values are + * being requested. + * @argument {string[]} propertyNames - The names of each of the properties + * whose values are being requested. + * These can be provided as an array of + * strings or as a list of string + * arguments following the object + * argument. + * @returns {external:Promise} - A promise fulfilled when all of the + * requested data has been received and set on the specified properties of + * the passed in object. + */ + getObjectProperties: { + value: function (object, propertyNames) { + if (!object) { + return Promise.resolve(null); + } + /* + Benoit: + + - If we create an object, properties that are not relations can't + be fetched. We need to make sure we don't actually try. + + + COUNTER: If an object created already has the info to make it's primary key, we should go forward. + + The real test could be that if it's mapped as a relationship, then we might be able to get something. + + - If a property is a relationship and it wasn't set on the object, + as an object, we can't get it either. + */ + // if(this.isObjectCreated(object)) { + // //Not much we can do there anyway, punt + // return Promise.resolve(true); + // } else + if (this.isRootService) { + // Get the data, accepting property names as an array or as a list + // of string arguments while avoiding the creation of any new array. + //var names = Array.isArray(propertyNames) ? propertyNames : Array.prototype.slice.call(arguments, 1), + var names = isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1; + return this._getOrUpdateObjectProperties(object, names, start, false); + } else { + return this.rootService.getObjectProperties(...arguments); + } + }, + }, + + /** + * perform getObjectProperties for each object with propertyNames + * @method + * @argument {object[]} objects - The objects whose property values are + * being requested. + * @argument {string[]} propertyNames - The names of each of the properties + * whose values are being requested. + * These can be provided as an array of + * strings or as a list of string + * arguments following the object + * argument. + * @returns {external:Promise} - A promise fulfilled when all of the + * requested data has been received and set on the specified properties of + * the passed in object. + */ + + getObjectsProperties: { + value: function (objects, propertyNames) { + if (!objects || objects.length === 0) { + return Promise.resolve(null); + } else { + let promises = [], + i, + countI, + names = isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1; + + for (i = 0, countI = objects.length; i < countI; i++) { + promises.push(this._getOrUpdateObjectProperties(objects[i], names, start, false)); + } + + return Promise.all(promises); + } + }, + }, + + getObjectPropertyExpressions: { + value: function (object, propertyValueExpressions) { + if (this.isRootService) { + // Get the data, accepting property names as an array or as a list + // of string arguments while avoiding the creation of any new array. + var expressions = isArray(propertyValueExpressions) ? propertyValueExpressions : arguments, + start = expressions === propertyValueExpressions ? 0 : 1, + promises = [], + self = this; + + for(let i=0, countI = expressions.length; (i 0 + ? childServices.length === 1 + ? childServices[0].fetchObjectProperty(object, propertyName, isObjectCreated, isUpdate) + : this.childServicesFetchObjectProperty(object, propertyName, isObjectCreated, isUpdate) + : this.nullPromise; + }, + }, + + childServicesFetchObjectProperty: { + value: function (object, propertyName, isObjectCreated, isUpdate) { + //Workaround for now: + if (object.dataIdentifier.dataService) { + return object.dataIdentifier.dataService.fetchObjectProperty(object, propertyName, isObjectCreated, isUpdate); + } + + throw "Data Services with multiple child services per ObjectDescriptor have to implement this method"; + // /* + // If there's more than one, we're entering the realm of decisions about how to deal with them. + // That's why MuxDataService's and it's subclasses were created, to implement the various possible strategies. + // */ + + // let childServices = this.childServicesForType(object.objectDescriptor), + // promises, + // childService; + + // if(childServices === 1) { + // return childServices[0].fetchObjectProperty(object, propertyName, isObjectCreated) + // } else { + // for(childService of childServices) { + // (promises || (promises = [])).push(childService.fetchObjectProperty(object, propertyName, isObjectCreated)); + // } + // //If the + // } + }, + }, + + _delegateFunctionForPropertyName: { + value: function (propertyName) { + var capitalized = propertyName.charAt(0).toUpperCase() + propertyName.slice(1), + functionName = "fetch" + capitalized + "Property"; + return typeof this[functionName] === "function" && this[functionName]; + }, + }, + + _isAsync: { + value: function (object) { + return object && object.then && typeof object.then === "function"; + }, + }, + + /* TODO: Remove when mapping is moved in the webworker. This is used right now to know + when an object is bieng mapped so we can avoid tracking changes happening during that time. This issue will disappear when mapping is done in a web worker and not on the object directly. + */ + _objectsBeingMapped: { + value: new CountedSet(), + }, + + _rawDataToObjectMappingPromiseByObject: { + value: new Map(), + }, + + _setMapRawDataToObjectPromise: { + value: function (rawData, object, promise) { + var objectMap = this._rawDataToObjectMappingPromiseByObject.get(object), + rawDataPromise; + if (!objectMap) { + objectMap = new Map(); + this._rawDataToObjectMappingPromiseByObject.set(object, objectMap); + } + + if (!objectMap.has(rawData)) { + objectMap.set(rawData, promise); + } else { + if (objectMap.get(rawData) !== promise) { + console.error( + "Overriding existing mapping promise for object:", + object, + " rawData:", + rawData, + " existing Promise:", + objectMap.get(rawData), + " new Promise:", + promise + ); + } else { + console.error( + "Overriding existing mapping promise with same promise for object:", + object, + " rawData:", + rawData, + " existing Promise:", + objectMap.get(rawData) + ); + } + } + }, + }, + + _getMapRawDataToObjectPromise: { + value: function (rawData, object) { + var objectMap = this._rawDataToObjectMappingPromiseByObject.get(object); + return objectMap && objectMap.get(rawData); + }, + }, + + _deleteMapRawDataToObjectPromise: { + value: function (rawData, object) { + var objectMap = this._rawDataToObjectMappingPromiseByObject.get(object); + // console.log(object.dataIdentifier.objectDescriptor.name+" _deleteMapRawDataToObjectPromise: id: "+object.dataIdentifier.primaryKey); + // if(!objectMap.has(rawData)) { + // console.log(object.dataIdentifier.objectDescriptor.name+" _deleteMapRawDataToObjectPromise: id: "+object.dataIdentifier.primaryKey+" !!! COULDN'T FIND rawData entry"); + // } + return objectMap && objectMap.delete(rawData); + }, + }, + + _fetchObjectPropertyWithPropertyDescriptor: { + value: function (object, propertyName, propertyDescriptor, isObjectCreated) { + var self = this, + objectDescriptor = this.objectDescriptorForObject(object), + mapping = objectDescriptor && this.mappingForType(objectDescriptor), + data = {}, + snapshotPromise, + result; + + if (mapping) { + /* + To open the ability to get derived values from non-saved objects, some failsafes blocking a non-saved created object to get any kind of property resolved/fetched were removed. So we need to be smarter here and do the same. + + If an object is created (which we don't know here, but we can check), fetching a property relies on the primary key and that the primarty key is one property only (like a uuid) and there's already a value (client-side generated like uuid can be), than it can't be fetched and we shoould resolve to null. + + Another approch would be to map all dependencies and let the rule's converter assess if it has everytrhing it needs to do the job, but at that level, the converter doesn't know the object, but it has the primaryKey in the criteria's syntax and the value in the associated parameters, so it could find out if there's a corresponding object that is created. It might be needed, let's see if this first heuristic works first. + */ + if (isObjectCreated) { + var rule = mapping.objectMappingRuleForPropertyName(propertyName), + rawDataPrimaryKeys = mapping.rawDataPrimaryKeys, + requiredRawProperties = rule && rule.requirements, + mappingResult; + + mappingResult = this.mapObjectToRawData(object, data); + if (Promise.is(mappingResult)) { + snapshotPromise = mappingResult; + } else { + snapshotPromise = Promise.resolve(mappingResult); + } + + /* + rawDataPrimaryKeys.length === 1 is to assess if it's a traditional id with no intrinsic meaning + */ + // if(rawDataPrimaryKeys.length === 1 && requiredRawProperties.indexOf(rawDataPrimaryKeys[0]) !== -1) { + if ( + rawDataPrimaryKeys.length === 1 && + requiredRawProperties.indexOf(rawDataPrimaryKeys[0]) === -1 + ) { + /* + Fetching depends on something that doesn't exists on the other side, we bail: + */ + return this.nullPromise; + } + } else { + /* + @marchant: Why aren't we passing this.snapshotForObject(object) instead of copying everying in a new empty object? + + @tejaede: The criteria source was a first attempt to support derived properties. It's a bucket in which we can put data from the cooked object in order to fetch other cooked properties. The cooked data placed in the criteria source does not belong in the snapshot. + + For example, take this model: + + A Foo model includes a bars property and a baz property. + Foo.baz is derived from the value of Foo.bars. + When the application triggers a fetch for Foo.baz, the value of Foo.bars needs to be available to build the criteria for the fetch. However, the value of Foo.bars is cooked and does not belong in the snapshot. Therefore, we create a copy of the snapshot called the "criteria source" into which we can put Foo.bars. + + All of this said, I don't know this is the right way to solve the problem. The issue at the moment is that this functionality is being used so we cannot remove it without an alternative. + */ + Object.assign(data, this.snapshotForObject(object)); + snapshotPromise = Promise.resolve(data); + } + + return snapshotPromise.then((data) => { + self._objectsBeingMapped.add(object); + + result = mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName, true); + if (this._isAsync(result)) { + return result.then( + function () { + Object.assign(data, self.snapshotForObject(object)); + result = mapping.mapRawDataToObjectProperty(data, object, propertyName); + + if (!self._isAsync(result)) { + result = this.nullPromise; + self._objectsBeingMapped.delete(object); + } else { + result = result.then( + function (resolved) { + self._objectsBeingMapped.delete(object); + return resolved; + }, + function (failed) { + self._objectsBeingMapped.delete(object); + } + ); + } + return result; + }, + function (error) { + self._objectsBeingMapped.delete(object); + throw error; + } + ); + } else { + //This was already done a few lines up. Why are we re-doing this? + Object.assign(data, self.snapshotForObject(object)); + /* + This code path ends up triggering the mapping logic to convert a foreign/primary key to an object via a converter. + The converter ends up being the one doing a fetch + */ + result = mapping.mapRawDataToObjectProperty(data, object, propertyName); + if (!this._isAsync(result)) { + /* + BUG: when we map from values already in the snapshot we end up here, but the value has alredy been assigned to the object by mapping.mapRawDataToObjectProperty(data, object, propertyName). + + So there's really nothing else to do but return result. If we re-set result to null bellow, it does end up as the result of DataTrigger._fetchObjectProperty() promise and erases the value that was there, the one just mapped, causing loss of data and bugs. + */ + // result = this.nullPromise; + this._objectsBeingMapped.delete(object); + } else { + result = result.then( + function (resolved) { + self._objectsBeingMapped.delete(object); + return resolved; + }, + function (failed) { + self._objectsBeingMapped.delete(object); + } + ); + } + return result; + } + }); + } else { + return this.nullPromise; + } + }, + }, + + /** + * @private + * @method + */ + _getOrUpdateObjectProperties: { + value: function (object, names, start, isUpdate) { + var triggers, trigger, promises, promise, i, n; + // Request each data value separately, collecting unique resulting + // promises into an array and a set, but avoid creating any array + // or set unless that's necessary. + triggers = this._getTriggersForObject(object); + for (i = start, n = names.length; i < n; i += 1) { + trigger = triggers && triggers[names[i]]; + promise = !trigger + ? this.nullPromise + : isUpdate + ? trigger.updateObjectProperty(object) + : trigger.getObjectProperty(object); + if (promise !== this.nullPromise) { + if (!promises) { + promises = { array: [promise] }; + } else if (!promises.set && promises.array[0] !== promise) { + promises.set = new Set(); + promises.set.add(promises.array[0]); + promises.set.add(promise); + promises.array.push(promise); + } else if (promises.set && !promises.set.has(promise)) { + promises.set.add(promise); + promises.array.push(promise); + } + } + } + + // Return a promise that will be fulfilled only when all of the + // requested data has been set on the object. If possible do this + // without creating any additional promises. + return !promises + ? this.nullPromise + : !promises.set + ? promises.array[0] + : Promise.all(promises.array).then(this.nullFunction); + }, + }, + + /** + * @private + * @method + */ + _setObjectPropertyValue: { + value: function (object, propertyName, propertyValue, updateInverse) { + var triggers = this._getTriggersForObject(object), + trigger = triggers ? triggers[propertyName] : null; + + if (trigger) { + trigger._setValue(object, propertyValue, false); + } + }, + }, + + /*************************************************************************** + * Data Object Creation + */ + + /** + * Find an existing data object corresponding to the specified raw data, or + * if no such object exists, create one. + * + * Since root services are responsible for tracking and creating data + * objects, subclasses whose instances will not be root services should + * override this method to call their root service's implementation of it. + * + * @method + * @argument {DataObjectDescriptor} type - The type of object to find or + * create. + * @argument {Object} rawData - An object whose property values + * hold the object's raw data. That + * data will be used to determine + * the object's unique identifier. + * @argument {?} context - A value, usually passed in to a + * [raw data service's]{@link RawDataService} + * [addRawData()]{@link RawDataService#addRawData} + * method, that can help in getting + * or creating the object. + * @returns {Object} - The existing object with the unique identifier + * specified in the raw data, or if no such object exists a newly created + * object of the specified type. + */ + getDataObject: { + value: function (type, rawData, dataIdentifier, context) { + if (this.isRootService) { + var dataObject; + // TODO [Charles]: Object uniquing. + if (this.isUniquing && dataIdentifier) { + dataObject = this.objectForDataIdentifier(dataIdentifier, rawData, context); + } + if (!dataObject) { + if (dataIdentifier === undefined) { + dataObject = this.createDataObject(type); + } else { + dataObject = this._createDataObject(type, dataIdentifier); + } + } + + return dataObject; + } else { + let dataObject = this.objectForDataIdentifier(dataIdentifier); + + if (!dataObject) { + dataObject = this.rootService.getDataObject(type, rawData, dataIdentifier, context); + + /* + If the root service uses a different dataIdentifier than ours, + We keep a reference for ourselves to the unique object returned + for our own dataIdentifier for it. + */ + // if(dataObject.dataIdentifier !== dataIdentifier) { + // this.recordObjectForDataIdentifier(dataObject, dataIdentifier); + // } + } + + return dataObject; + } + }, + }, + + isUniquing: { + value: false, + }, + + _identifier: { + value: undefined, + }, + + identifier: { + get: function () { + return this._identifier || (this._identifier = Montage.getInfoForObject(this).moduleId); + }, + set: function (value) { + if (value !== this._identifier) { + this._identifier = value; + } + }, + }, + + __dataIdentifierByObject: { + value: null, + }, + + _dataIdentifierByObject: { + // This property is shared with all child services. + // If created lazily the wrong data identifier will be returned when + // accessed by a child service. + + /* + Benoit 2/13/2025. Going for a per service bookkeeping, so rootService is the one creating and uniquing + objects, but RawDataServices can keep track with their own native dataIdentifiers + */ + get: function () { + return this.__dataIdentifierByObject || (this.__dataIdentifierByObject = new WeakMap()); + }, + }, + + /** + * Returns a unique DataIdentifier for an object + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead + * of this method. That method will be called by this method when needed. + * + * @method + * @argument {object} object - The object for which a DataIdentifier is + * being requested. + * + * @returns {DataIdentifier} - An object's DataIdentifier + */ + dataIdentifierForObject: { + value: function (object) { + //If we don't have a local / native one, we return the one assigned by the main service + //let dataIdentifier = this._dataIdentifierByObject.get(object) || object.dataIdentifier; + let dataIdentifier = this._dataIdentifierByObject.get(object); + + //When we merge in a graph of data object's it's possible that those objects are + if (!dataIdentifier && this !== this.mainService) { + dataIdentifier = this.mainService.dataIdentifierForObject(object); + if (!dataIdentifier) { + // throw "No dataIdentifier found for data object: "+object; + dataIdentifier = this.createDataIdentifierForObject(object); + if (this.parentService === this.mainService) { + this.mainService.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + this.mainService.recordDataIdentifierForObject(dataIdentifier, object); + } + } + this.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + } + return dataIdentifier; + }, + }, + + createDataIdentifierForObject: { + value: function (object) { + return this.dataIdentifierForNewObjectWithObjectDescriptor(object.objectDescriptor); + }, + }, + + /** + * Records an object's DataIdentifier + * + * @method + * @argument {object} object - an Object. + * @argument {DataIdentifier} dataIdentifier - The object whose property values are + */ + recordDataIdentifierForObject: { + value: function (dataIdentifier, object) { + /* + When we have a SynchronizationDataService in-between MainService and RawDataOnes, dataIdentifier and + this._dataIdentifierByObject.get(object) are actually not the same. need to figure out why, but narrowing the test + to verify they have the same primaryKey should help for now. + */ + if ( + this._dataIdentifierByObject.has(object) && + this._dataIdentifierByObject.get(object)?.primaryKey !== dataIdentifier.primaryKey + ) { + //throw new Error("recordDataIdentifierForObject when one already exists:"+JSON.stringify(object)); + console.error( + "WARNING: recordDataIdentifierForObject when one already exists:" + JSON.stringify(object) + ); + } + /* + TODO: This is called twice when this._dataIdentifierByObject already contains (object, dataIdentifier) + */ + this._dataIdentifierByObject.set(object, dataIdentifier); + }, + }, + + /** + * Remove an object's DataIdentifier + * + * @method + * @argument {object} object - an object + */ + removeDataIdentifierForObject: { + value: function (object) { + // console.log("removeDataIdentifierForObject(",object); + this._dataIdentifierByObject.delete(object); + }, + }, + + __objectByDataIdentifier: { + value: null, + }, + + _objectByDataIdentifier: { + get: function () { + return this.__objectByDataIdentifier || (this.__objectByDataIdentifier = new WeakMap()); + }, + }, + + clearObjectDataIdentifiers: { + value: function () { + this.__objectByDataIdentifier = null; + this.__dataIdentifierByObject = null; + }, + }, + /** + * Returns a unique object for a DataIdentifier + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} instead + * of this method. That method will be called by this method when needed. + * + * @method + * @argument {DataIdentifier} dataIdentifier - the dataIdentifier object + * @argument {Object} rawData - the raw data for the object expected to be found + * @argument {Object} context - the context around, typically a DataOperation + * @returns {Objecr} - object found for that dataIdentifier if any + */ + objectForDataIdentifier: { + value: function (dataIdentifier, rawData, context) { + return this._objectByDataIdentifier.get(dataIdentifier); + }, + }, + /** + * Records an object's DataIdentifier + * + * @method + * @argument {DataIdentifier} dataIdentifier - DataIdentifier + * @argument {object} object - object represented by dataIdentifier + */ + recordObjectForDataIdentifier: { + value: function (object, dataIdentifier) { + this._objectByDataIdentifier.set(dataIdentifier, object); + }, + }, + + /** + * Remove an object's DataIdentifier + * + * @method + * @argument {object} object - an object + */ + removeObjectForDataIdentifier: { + value: function (dataIdentifier) { + this._objectByDataIdentifier.delete(dataIdentifier); + }, + }, + _eventPoolFactoryForEventType: { + value: function () { + return new DataEvent(); + }, + }, + + __dataEventPoolByEventType: { + value: null, + }, + _resetDataEvent: { + value: function(dataEvent) { + //The rest is overriden every time + //For willSave... + delete dataEvent["_composedPath"]; + + //For change event... Needs a different reset function per type... + delete dataEvent["transaction"]; + + } + }, + _dataEventPoolForEventType: { + value: function (eventType) { + var pool = (this.__dataEventPoolByEventType || (this.__dataEventPoolByEventType = new Map())).get( + eventType + ); + if (!pool) { + this.__dataEventPoolByEventType.set( + eventType, + (pool = new ObjectPool(this._eventPoolFactoryForEventType, this._resetDataEvent)) + ); + } + return pool; + }, + }, + __preparedConstructorsForDataEvents: { + value: null, + }, + + isConstructorPreparedToHandleDataEvents: { + value: function (objectConstructor) { + return ( + this.__preparedConstructorsForDataEvents || (this.__preparedConstructorsForDataEvents = new Set()) + ).has(objectConstructor); + }, + }, + + prepareConstructorToHandleDataEvents: { + value: function (objectConstructor, event) { + if (typeof objectConstructor.prepareToHandleDataEvents === "function") { + objectConstructor.prepareToHandleDataEvents(event); + } + //prepareToHandleDataEvent or prepareToHandleCreateEvent + this.__preparedConstructorsForDataEvents.add(objectConstructor); + }, + }, + + dispatchDataEventTypeForObject: { + value: function (eventType, object, detail, composedPath) { + /* + This needs to be made more generic in EventManager, which has "prepareForActivationEvent, + but it's very specialized for components. Having all prototypes of DO register as eventListeners upfront + would be damaging performance wise. We should do it as things happen. + */ + if (object.dispatchEvent) { + var eventPool = this._dataEventPoolForEventType(eventType), + objectDescriptor = this.objectDescriptorForObject(object), + objectConstructor = object.constructor, + dataEvent = eventPool.checkout(); + + dataEvent.type = eventType; + dataEvent.target = objectDescriptor; + dataEvent.dataService = this; + dataEvent.dataObject = object; + dataEvent.detail = detail; + if(composedPath && dataEvent.setComposedPath) { + dataEvent.setComposedPath(composedPath); + } + + if (!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { + this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); + } + + object.dispatchEvent(dataEvent); + + var propagationPromise = dataEvent.propagationPromise; + if (Promise.is(propagationPromise)) { + return propagationPromise.then(function () { + eventPool.checkin(dataEvent); + }); + } else { + eventPool.checkin(dataEvent); + } + } + }, + }, + + /** + * Create a new data object of the specified type. + * + * Since root services are responsible for tracking and creating data + * objects, subclasses whose instances will not be root services should + * override this method to call their root service's implementation of it. + * + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + //TODO add the creation of a temporary identifier to pass to _createDataObject + createDataObject: { + value: function (type) { + if (this.isRootService) { + var service = this.childServiceForType(type), + //Gives a chance to raw data service to provide a primary key for clien-side creation/ + //Especially useful for systems that use uuid as primary keys. + //object = this._createDataObject(type, service.dataIdentifierForNewObjectWithObjectDescriptor(type)) + object = this._createDataObject( + type, + service.dataIdentifierForNewObjectWithObjectDescriptor(this.objectDescriptorForType(type)) + ); + + this.registerCreatedDataObject(object); + + return object; + } else { + this.rootService.createDataObject(type); + } + }, + }, + + /** + * A DataObject will always be decribed by only one ObjectDescriptor, even when we support more "main" dataServices. + * So caching on prototype so it serves everyone. + * + * @private + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + + _objectDescriptorForObjectCache: { + value: new WeakMap(), + }, + + /** + * A WeakMap where keys are ObjectDescriptors and values are all instances of data objects of that type + * + * @private + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + + _objectsByObjectDescriptor: { + value: new WeakMap(), + }, + + /** + * ObjectDescriptors' instances are held in an array so we can iterate on them. But this may hold them and prevent garbage collection, as ObjectDesc + * + * @private + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + registeredObjectsForObjectDescriptor: { + value: function (anObjectDescriptor) { + return ( + this._objectsByObjectDescriptor.get(anObjectDescriptor) || + (this._objectsByObjectDescriptor.set(anObjectDescriptor, []) && + this._objectsByObjectDescriptor.get(anObjectDescriptor)) + ); + }, + }, + + registeredObjectsForObjectDescriptorMatchingCriteria: { + value: function (anObjectDescriptor, aCriteria, _result) { + let result = _result || [], + anObjectDescriptorResult, + aCriteriaPredicateFunction, + registeredObjectsForObjectDescriptor = + this.registeredObjectsForObjectDescriptor(anObjectDescriptor), + childObjectDescriptors = anObjectDescriptor.childObjectDescriptors; + + if (registeredObjectsForObjectDescriptor) { + aCriteriaPredicateFunction = aCriteria.predicateFunction; + anObjectDescriptorResult = registeredObjectsForObjectDescriptor.filter(aCriteriaPredicateFunction); + if (anObjectDescriptorResult && anObjectDescriptorResult.length) { + result.push(...anObjectDescriptorResult); + } + } + + if (childObjectDescriptors) { + //There could be childObjectDescriptors that we need to take care of as well. + for (let i = 0, countI = childObjectDescriptors.length; i < countI; i++) { + this.registeredObjectsForObjectDescriptorMatchingCriteria( + childObjectDescriptors[i], + aCriteria, + result + ); + } + } + + return result; + }, + }, + + registerDataObject: { + value: function (aDataObject) { + this.registeredObjectsForObjectDescriptor(aDataObject.objectDescriptor).push(aDataObject); + }, + }, + + unregisterDataObject: { + value: function (aDataObject) { + this.registeredObjectsForObjectDescriptor(aDataObject.objectDescriptor).delete(aDataObject); + }, + }, + + unregisterDataObjectsWithObjectDescriptor: { + value: function (anObjectDescriptor) { + this._objectsByObjectDescriptor.delete(anObjectDescriptor); + }, + }, + + /** + * Create a data object without registering it in the new object map. + * + * @private + * @method + * @argument {DataObjectDescriptor} type - The type of object to create. + * @returns {Object} - The created object. + */ + _createDataObject: { + value: function (type, dataIdentifier) { + var objectDescriptor = this.objectDescriptorForType(type), + object = Object.create(this._getPrototypeForType(objectDescriptor)), + dataIdentifierDataService = dataIdentifier.dataService, + delegateDataIdentifier; + // constructor = this._getPrototypeForType(objectDescriptor).constructor, + // object = new constructor; + // //object = Reflect.construct(constructor, this._emptyArray); + + if (object) { + delegateDataIdentifier = + dataIdentifierDataService.callDelegateMethod( + "dataIdentifierForRawDataServiceCreatingObjectWithDataIdentifier", + dataIdentifier.dataService, + dataIdentifier + ) ?? dataIdentifier; + + /* + Our delegate overrode our dataIdentifier, we're going to keep a reference from + dataIdentifier to the object + */ + if (delegateDataIdentifier !== dataIdentifier) { + this.mainService.recordObjectForDataIdentifier(object, dataIdentifier); + dataIdentifier = delegateDataIdentifier; + } + //This needs to be done before a user-land code can attempt to do + //anything inside its constructor, like creating a binding on a relationships + //causing a trigger to fire, not knowing about the match between identifier + //and object... If that's feels like a real situation, it is. + this.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + // if (dataIdentifier && this.isUniquing) { + // this.recordDataIdentifierForObject(dataIdentifier, object); + // this.recordObjectForDataIdentifier(object, dataIdentifier); + // } + + //This can't work with ES Classes + //object = object.constructor.call(object) || object; + if (object) { + this._setObjectType(object, objectDescriptor); + this._objectDescriptorForObjectCache.set(object, objectDescriptor); + + //This is an in-memory cache of all objects with the same objectDescriptor, regarding of the fact that are new objects or fetched + this.registerDataObject(object); + } + + dataIdentifierDataService.callDelegateMethod( + "rawDataServiceDidCreateObject", + dataIdentifierDataService, + object + ); + } + return object; + }, + }, + + /** + * Register an object with its dataIdentifier for uniquing reasons + * + * @private + * @method + * @argument {Object} object - object to register. + * @argument {DataIdentifier} dataIdentifier - dataIdentifier of object to register. + * @returns {void} + */ + registerUniqueObjectWithDataIdentifier: { + value: function (object, dataIdentifier) { + //Benoit: this is currently relying on a manual turn-on of isUniquing on the MainService, which is really not something people should have to worry about... + /* + Benoit 2/13/25: Relaxing to have RawDataServices keep track of this in a context + where mutliple RawDataServices for the same ObjectDescriptors are involved + with different native RawData shapes + */ + //if (object && dataIdentifier && this.isRootService && this.isUniquing) { + if (object && dataIdentifier) { + this.recordDataIdentifierForObject(dataIdentifier, object); + this.recordObjectForDataIdentifier(object, dataIdentifier); + } + }, + }, + + /*************************************************************************** + * + * Data Object Changes + * + */ + + /** + * A set of the data objects created by this service or any other descendent + * of this service's [root service]{@link DataService#rootService} since + * that root service's data was last saved, or since the root service was + * created if that service's data hasn't been saved yet. + * + * Since root services are responsible for tracking data objects, subclasses + * whose instances will not be root services should override this property + * to return their root service's value for it. + * + * @type {Set.} + */ + _objectDescriptorsWithChanges: { + value: undefined, + }, + objectDescriptorsWithChanges: { + get: function () { + return this._objectDescriptorsWithChanges || (this._objectDescriptorsWithChanges = new CountedSet()); + }, + }, + createdDataObjects: { + get: function () { + if (this.isRootService) { + if (!this._createdDataObjects) { + this._createdDataObjects = new Map(); + } + return this._createdDataObjects; + } else { + return this.rootService.createdDataObjects; + } + }, + }, + + registerCreatedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + createdDataObjects = this.createdDataObjects, + value = createdDataObjects.get(objectDescriptor); + if (!value) { + createdDataObjects.set(objectDescriptor, (value = new Set())); + } + + /* + This makes sure that properties' data triggers' valueStatus are set to null + ensuring there's no reference to it in a storage + */ + //////////this._setCreatedObjectPropertyTriggerStatusToNull(dataObject); + + value.add(dataObject); + this.objectDescriptorsWithChanges.add(objectDescriptor); + + this.dispatchDataEventTypeForObject(DataEvent.create, dataObject); + }, + }, + + unregisterCreatedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + value = this.createdDataObjects.get(objectDescriptor); + if (value) { + value.delete(dataObject); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + }, + }, + + _setCreatedObjectPropertyTriggerStatusToNull: { + value: function (dataObject) { + let dataObjectTriggers = this._getTriggersForObject(dataObject), + dataObjectTriggersPropertyNames = Object.keys(dataObjectTriggers); + + for ( + let i = 0, iPropertyName, iValue, countI = dataObjectTriggersPropertyNames.length; + i < countI; + i++ + ) { + iPropertyName = dataObjectTriggersPropertyNames[i]; + //If values were set on the object and the property has a trigger, meaning it's handled by Mod data + if (!dataObject.hasOwnProperty(iPropertyName)) { + dataObjectTriggers[iPropertyName]._setValueStatus(dataObject, null); + } + } + }, + }, + + /** + * This method makes the mainService aware of a dataObject that wasn't fetched. So + * beyond that we can't really know if it's new, as it doesn't exists in the a storage + * the matching data services encapsulate, or if it does and this might be more of a merge + * / update. It might be necessary to flag it to rawDataServcices somehow. A DB like PostgreSQL + * can probably deal with it by using INSERT ... ON CONFLICT (key) DO UPDATE (and ON CONFLICT (key) DO NOTHING), + * i.e. upsert, or the more recent MERGE. + * + * TODO: refactor to use isObjectCreated() and registerCreatedDataObject() methods if possible + * + * @method {Set.} + * @argument {DataObject} dataObject + * @argument {Object} delegate - an object that gets a chance to substitute an existing object instead of merging the one at end, for example to avoid duplicates + * @returns {void || Promise} - if a delegate is used, the method will return a promise otherwise it returns the dataObject passed in + + */ + + _invokeDelegateWillMergeDataObjectPropertyValueIntoExistingDataObject: { + value: function ( + delegate, + dataObject, + propertyName, + valueToMerge, + existingDataObject, + propertyArray, + propertyIndex, + _promises, + isRoot, + _mergingDataObjects + ) { + if (!delegate?.willMergeDataObjectPropertyValueIntoExistingDataObject) { + this.mergeDataObject(valueToMerge, delegate, _promises, isRoot, _mergingDataObjects); + } else { + let delegatePromise = delegate + .willMergeDataObjectPropertyValueIntoExistingDataObject( + dataObject, + propertyName, + valueToMerge, + existingDataObject, + propertyArray, + propertyIndex + ) + .then((delegateValueToMerge) => { + /* + If null is returned by the delegate, nothing to do + But if an object is returned that is known, then the + next mergeDataObject() will punt as needed + */ + if (delegateValueToMerge) { + this.mergeDataObject( + delegateValueToMerge, + delegate, + _promises, + isRoot, + _mergingDataObjects + ); + + /* + Update the property + DOES NOT HANDLE ANYTHING BUT to-one and array-based to-many... + Shall we leave this to the delegate to do? + */ + if (propertyArray) { + if (!existingDataObject) { + if (propertyArray[propertyIndex] !== delegateValueToMerge) { + propertyArray.set(propertyIndex, delegateValueToMerge); + } + } else { + return this.getObjectProperties(existingDataObject, [propertyName]).then(() => { + let existingDataObjectArray = existingDataObject[propertyName]; + if (!existingDataObjectArray.includes(delegateValueToMerge)) { + existingDataObjectArray.push(delegateValueToMerge); + } + }); + } + } else { + if (existingDataObject) { + return this.getObjectProperties(existingDataObject, [propertyName]).then(() => { + let existingDataObjectValue = existingDataObject[propertyName]; + if (existingDataObjectValue !== delegateValueToMerge) { + existingDataObjectValue[propertyName] = delegateValueToMerge; + } + }); + } else if (dataObject[propertyName] !== delegateValueToMerge) { + dataObject[propertyName] = delegateValueToMerge; + } + } + } + + return delegateValueToMerge; + }); + + _promises.push(delegatePromise); + return delegatePromise; + } + }, + }, + + _invokeDelegateWillMergeDataObject: { + value: function (delegate, dataObject, _promises, isRoot) { + if (delegate?.willMergeDataObject) { + let delegatePromise = delegate.willMergeDataObject(dataObject); + _promises.push(delegatePromise); + return delegatePromise; + } else { + return Promise.resolve(dataObject); + } + }, + }, + + _mergeDataObjectProperties: { + value: function (dataObject, delegate, delegateObjectToMerge, _promises, _isRoot, _mergingDataObjects) { + //Sanity check + if (delegateObjectToMerge === dataObject) { + delegateObjectToMerge = null; + } + + let objectKeys = Object.keys(dataObject); + for (let iKey of objectKeys) { + let iValue = dataObject[iKey]; + //console.log("merging "+iKey+", iValue ",iValue, "for ",dataObject.identifier); + + //If it's a new object, we need to make sure things are square like if dataObject had been created by the data service + if (!delegateObjectToMerge) { + delete dataObject[iKey]; + dataObject[iKey] = iValue; + + //The array on dataObject is its owm, so we need to grab it as it's not iValue anymore + iValue = dataObject[iKey]; + } + + //If we have delegateObjectToMerge, we need to make sure that we move the values of dataObject on to delegateObjectToMerge + if (isArray(iValue)) { + for (let i = 0, countI = iValue.length; i < countI; i++) { + if (delegate) { + this._invokeDelegateWillMergeDataObjectPropertyValueIntoExistingDataObject( + delegate, + dataObject, + iKey, + iValue[i], + delegateObjectToMerge, + iValue, + i, + _promises, + _isRoot, + _mergingDataObjects + ); + } else { + //We don't change the structure, we just blend in values off dataObject + this.mergeDataObject(iValue[i], delegate, _promises, _isRoot, _mergingDataObjects); + } + } + } else if (iValue && typeof iValue === "object") { + if (delegate) { + this._invokeDelegateWillMergeDataObjectPropertyValueIntoExistingDataObject( + delegate, + dataObject, + iKey, + iValue, + delegateObjectToMerge, + undefined, + undefined, + _promises, + _isRoot, + _mergingDataObjects + ); + } else { + //We don't change the structure, we just blend in values off dataObject + this.mergeDataObject(iValue, delegate, _promises, _isRoot, _mergingDataObjects); + } + } + + //console.log("merged "+iKey+" for:"+dataObject.identifier); + } + }, + }, + + _mergeDataObject: { + value: function (dataObject, delegate, delegateObjectToMerge, _promises, _isRoot, _mergingDataObjects) { + if (!delegateObjectToMerge || (delegateObjectToMerge && dataObject === delegateObjectToMerge)) { + let objectDescriptor = this.objectDescriptorForObject(dataObject), + createdDataObjects = this.createdDataObjects, + value = createdDataObjects.get(objectDescriptor); + + if (!value) { + createdDataObjects.set(objectDescriptor, (value = new Set())); + this.objectDescriptorsWithChanges.add(objectDescriptor); + } + + if (!value.has(dataObject)) { + let service = this.childServicesThatCanSaveDataType(objectDescriptor)?.[0], + // //In case the object's primarykey is a public property + // // + // //primaryKey = service.dataIdentifierForNewObjectWithObjectDescriptor(objectDescriptor, dataObject), + // primaryKey = service.primaryKeyForTypeRawData(objectDescriptor, dataObject), + prototype = this._getPrototypeForType(objectDescriptor /*, service*/); + // dataIdentifier = primaryKey + // ? service.dataIdentifierForTypePrimaryKey(objectDescriptor, primaryKey) + // : service.dataIdentifierForNewObjectWithObjectDescriptor(objectDescriptor); + /* + for Anonymous identity object, it happens that no service is found, which is to be investigated. + One unusual thing is that Identity extends Montage and not DataObject, relevant? + */ + if (service) { + let dataIdentifier = service.dataIdentifierForObject(dataObject); + // this is mainService here + // this.registerUniqueObjectWithDataIdentifier(dataObject, dataIdentifier); + // this.recordDataIdentifierForObject(dataIdentifier, dataObject); + + if (!dataIdentifier) { + dataIdentifier = service?.dataIdentifierForNewObjectWithObjectDescriptor( + objectDescriptor, + dataObject + ); + service.registerUniqueObjectWithDataIdentifier(dataObject, dataIdentifier); + this.registerUniqueObjectWithDataIdentifier(dataObject, dataIdentifier); + this.recordDataIdentifierForObject(dataIdentifier, dataObject); + } + + //Need to fix the prototype if needed + if (Object.getPrototypeOf(dataObject) !== prototype) { + // let valueShallowCopy = {}; + // Object.assign(valueShallowCopy, dataObject); + Object.setPrototypeOf(dataObject, prototype); + } + + value.add(dataObject); + + //This is done in _createDataObject, is it needed here? + this._setObjectType(dataObject, objectDescriptor); + this._objectDescriptorForObjectCache.set(dataObject, objectDescriptor); + + //Make sure we won't fetch properties missing + this._setCreatedObjectPropertyTriggerStatusToNull(dataObject); + + //Let's make sure the values are now handled by DataTriggers + this._mergeDataObjectProperties( + dataObject, + delegate, + delegateObjectToMerge, + _promises, + _isRoot, + _mergingDataObjects + ); + + //Build the changes we'll want to upsert + //this.registerMergedDataObjectChanges(dataObject); + + this.dispatchDataEventTypeForObject(DataEvent.create, dataObject); + + //if(delegate && _isRoot && _promises) { + if (_isRoot && _promises) { + return Promise.all(_promises).then(() => dataObject); + } + return dataObject; + } else { + console.warn( + "WARNING: Object NOT MERGED: No RawDataService found that can save data type " + + objectDescriptor + ); + } + } + } else { + //An existing object was found, but we still want to eventually move properties from dataObject to delegateObjectToMerge + this._mergeDataObjectProperties( + dataObject, + delegate, + delegateObjectToMerge, + _promises, + _isRoot, + _mergingDataObjects + ); + } + + //if(delegate && _isRoot && _promises) { + if (_isRoot && _promises) { + return Promise.all(_promises).then(() => dataObject); + } + return dataObject; + }, + }, + + mergeDataObject: { + value: function (dataObject, delegate, _promises, _isRoot, _mergingDataObjects) { + let isRoot = arguments.length === 4 ? _isRoot : _promises === undefined, + promises; + + if (_promises) { + promises = _promises; + } + + if (!_mergingDataObjects) { + _mergingDataObjects = new Set(); + } + + if (_mergingDataObjects.has(dataObject)) { + return isRoot ? Promise.resolve(dataObject) : dataObject; + } else { + _mergingDataObjects.add(dataObject); + } + + if (dataObject === null || dataObject === undefined) { + //return (delegate && isRoot)? Promise.resolve(dataObject) : dataObject; + return isRoot ? Promise.resolve(dataObject) : dataObject; + } + + let objectDescriptor = this.objectDescriptorForObject(dataObject); + let childServicesForType = this.childServicesForType(objectDescriptor); + + if ( + !objectDescriptor || + (objectDescriptor && + (!this.handlesType(objectDescriptor) || + !childServicesForType || + (childServicesForType && + childServicesForType?.filter((value) => value.handlesType(objectDescriptor))?.length == + 0))) + ) { + //return (delegate && isRoot) ? Promise.resolve(dataObject) : dataObject; + return isRoot ? Promise.resolve(dataObject) : dataObject; + } + //If we already know about the object, nothing to do + else if ( + this.isObjectCreated(dataObject) || + this.isObjectFetched(dataObject) || + this.isObjectChanged(dataObject) || + this.isObjectDeleted(dataObject) + ) { + //return (delegate && isRoot) ? Promise.resolve(dataObject) : dataObject; + return isRoot ? Promise.resolve(dataObject) : dataObject; + } else { + let rootPromise; + + if (delegate) { + return this._invokeDelegateWillMergeDataObject( + delegate, + dataObject, + promises || (promises = []), + isRoot + ).then((delegateObjectToMerge) => { + this._mergeDataObject( + dataObject, + delegate, + delegateObjectToMerge, + promises || (promises = []), + isRoot, + _mergingDataObjects + ); + + if (isRoot && promises) { + return Promise.all(promises).then(() => dataObject); + } else { + return dataObject; + } + }); + } else { + this._mergeDataObject( + dataObject, + delegate, + undefined, + promises || (promises = []), + isRoot, + _mergingDataObjects + ); + + // if(delegate && isRoot && promises) { + if (isRoot && promises) { + return Promise.all(promises).then(() => dataObject); + } else { + return dataObject; + } + } + } + }, + }, + + registerMergedDataObjectChanges: { + value: function (dataObject) { + let dataObjectTriggers = this._getTriggersForObject(dataObject), + dataObjectTriggerNames = Object.keys(dataObjectTriggers), + dataObjectPropertyNames = Object.keys(dataObject), + // propertyNames = Object.keys(dataObjectTriggers), + changesForDataObject = this.changesForDataObject(dataObject); + + for ( + let i = 0, iDataObjectTrigger, iPropertyName, iValue, countI = dataObjectTriggerNames.length; + i < countI; + i++ + ) { + iDataObjectTrigger = dataObjectTriggers[dataObjectTriggerNames[i]]; + //If values were set on the object and the property has a trigger, meaning it's handled by Mod data + if (dataObject.hasOwnProperty((iPropertyName = iDataObjectTrigger._privatePropertyName))) { + changesForDataObject.set( + iDataObjectTrigger._propertyName, + (iValue = dataObject[iPropertyName]) + ); + } else if (dataObject.hasOwnProperty((iPropertyName = iDataObjectTrigger._propertyName))) { + changesForDataObject.set( + iDataObjectTrigger._propertyName, + (iValue = dataObject[iPropertyName]) + ); + } + + if (iValue) { + this.mergeDataObject(iValue); + } + } + + // for(let i=0, iPropertyName, iValue, countI = dataObjectPropertyNames.length; (i < countI); i++) { + // iPropertyName = dataObjectPropertyNames[i]; + // //If values were set on the object and the property has a trigger, meaning it's handled by Mod data + // if(dataObject.hasOwnProperty(iPropertyName) && dataObjectTriggers[iPropertyName]) { + // changesForDataObject.set(iPropertyName, (iValue = dataObject[iPropertyName])); + // if(iValue) { + // this.mergeDataObject(iValue); + // } + // } + // } + }, + }, + + isObjectCreated: { + value: function (object) { + var objectDescriptor = this.objectDescriptorForObject(object), + createdDataObjects = this.createdDataObjects.get(objectDescriptor), + isObjectCreated = createdDataObjects && createdDataObjects.has(object); + + if (!isObjectCreated) { + var service = this._getChildServiceForObject(object); + if (service) { + /* + !service.hasSnapshotForObject(object) means that it's not been fetched. + Not that it's been registered as created: + */ + isObjectCreated = service.isObjectCreated(object) /*|| !service.hasSnapshotForObject(object)*/; + } else { + isObjectCreated = false; + } + } + + if (!isObjectCreated && this.isMainService) { + var pendingTransactions = this._pendingTransactions; + + if (pendingTransactions && pendingTransactions.length) { + for (var i = 0, countI = pendingTransactions.length; i < countI; i++) { + if (pendingTransactions[i].createdDataObjects.get(object.objectDescriptor)?.has(object)) { + return true; + } + } + return false; + } else { + return false; + } + } + + return isObjectCreated; + }, + }, + + __readOnlyDataObjectsByObjectDescriptors: { + value: undefined, + }, + + _readOnlyDataObjectsByObjectDescriptors: { + get: function () { + return ( + this.__readOnlyDataObjectsByObjectDescriptors || + (this.__readOnlyDataObjectsByObjectDescriptors = new Map()) + ); + }, + }, + + _readOnlyDataObjectsFoObjectDescriptor: { + value: function (objectDescriptor) { + var result; + return ( + this._readOnlyDataObjectsByObjectDescriptors.get(objectDescriptor) || + (this._readOnlyDataObjectsByObjectDescriptors.set(objectDescriptor, (result = [])) && result) + ); + }, + }, + registerReadOnlyDataObject: { + value: function (dataObject) { + this._readOnlyDataObjectsFoObjectDescriptor(dataObject.objectDescriptor).push(dataObject); + }, + }, + unregisterReadOnlyDataObject: { + value: function (dataObject) { + if (dataObject) { + let readOnlyDataObjectsRegisteredForObjectDescriptor = + this.__readOnlyDataObjectsByObjectDescriptors?.(dataObject.objectDescriptor); + + if (readOnlyDataObjectsRegisteredForObjectDescriptor) { + let index = readOnlyDataObjectsRegisteredForObjectDescriptor.indexOf(dataObject); + + if (index !== -1) { + readOnlyDataObjectsRegisteredForObjectDescriptor.splice(index, 1); + } + } + } + }, + }, + + readOnlyDataObjectsRegisteredForObjectDescriptor: { + value: function (objectDescriptor) { + var result; + return this.__readOnlyDataObjectsByObjectDescriptors?.get(objectDescriptor); + }, + }, + + /** + * A set of the data objects modified by the user after they were fetched. + * * + * @type {Set.} + */ + changedDataObjects: { + get: function () { + if (this.isRootService) { + if (!this._changedDataObjects) { + this._changedDataObjects = new Map(); + } + return this._changedDataObjects; + } else { + return this.rootService.changedDataObjects; + } + }, + }, + registerChangedDataObject: { + value: function (dataObject, _changedDataObjects, _objectDescriptorsWithChanges) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + changedDataObjects, + value; + + if (this.isObjectCreated(dataObject)) { + console.warn( + `DataService can't register a new object (${objectDescriptor.name}) in changedDataObjects` + ); + return; + } + + changedDataObjects = _changedDataObjects ? _changedDataObjects : this.changedDataObjects; + value = changedDataObjects.get(objectDescriptor); + + if (!value) { + changedDataObjects.set(objectDescriptor, (value = new Set())); + } + value.add(dataObject); + + (_objectDescriptorsWithChanges ? _objectDescriptorsWithChanges : this.objectDescriptorsWithChanges).add(objectDescriptor); + }, + }, + + isObjectChanged: { + value: function (dataObject) { + return this.changedDataObjects.get(dataObject.objectDescriptor)?.has(dataObject); + }, + }, + + /* + This isn't used right now, but when we start using the undo manager + we probably will need to invoke it + */ + unregisterChangedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + value = this.changedDataObjects.get(objectDescriptor); + + if (value) { + value.delete(dataObject); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + }, + }, + + isObjectFetched: { + value: function (dataObject) { + return !!dataObject.snapshot; + }, + }, + + /** + * A Map of the data objects managed by this service or any other descendent + * of this service's [root service]{@link DataService#rootService} that have + * been changed since that root service's data was last saved, or since the + * root service was created if that service's data hasn't been saved yet + * + * Since root services are responsible for tracking data objects, subclasses + * whose instances will not be root services should override this property + * to return their root service's value for it. + * + * The key are the objects, the value is a Set containing the property changed + * + * @type {Map.} + */ + dataObjectChanges: { + get: function () { + if (this.isRootService) { + return this._dataObjectChanges || (this._dataObjectChanges = new Map()); + } else { + return this.rootService.dataObjectChanges; + } + }, + }, + + _dataObjectChanges: { + value: undefined, + }, + + /** + * A Map containing the changes for an object. Keys are the property modified, + * values are either a single value, or a map with added/removed keys for properties + * that have a cardinality superior to 1. The underlyinng collection doesn't matter + * at that level. + * + * Retuns undefined if no changes have been registered. + * + * @type {Map.} + */ + + _buildChangesForDataObject: { + value: function (dataObject) { + let changesForDataObject = new Map(); + this.dataObjectChanges.set(dataObject, changesForDataObject); + return changesForDataObject; + }, + }, + changesForDataObject: { + value: function (dataObject) { + return this.dataObjectChanges.get(dataObject) || this._buildChangesForDataObject(dataObject); + }, + }, + + /** + * handles the propagation of a value set to a property's inverse property, fka "addToBothSidesOfRelationship..." + * This doesn't handle range changes, which is done in another method. + * + * WARNING: It doesn't look like this is called anywhere, but _setDataObjectPropertyDescriptorValueForInversePropertyDescriptor() is + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _setDataObjectPropertyDescriptorValueForInversePropertyName: { + value: function (dataObject, propertyDescriptor, value, inversePropertyName) { + //Now set the inverse if any + if (inversePropertyName) { + var inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor; /* Sync */ + if (!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function ( + inversePropertyDescriptorResolved + ) { + self._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptorResolved + ); + }); + } else { + this._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor + ); + return Promise.resolveTrue; + } + } + }, + }, + + _setDataObjectPropertyDescriptorValueForInversePropertyDescriptor: { + value: function ( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor, + previousValue, + isDataObjectBeingMapped + ) { + if (!inversePropertyDescriptor) { + return; + } + + // if(this._objectsBeingMapped.has(dataObject) && this._objectsBeingMapped.has(value)) { + // return; + // } + + let addedValueAsObjectsBeingMapped = false, + setInversePromise; + + if (this._objectsBeingMapped.has(dataObject)) { + if (this._objectsBeingMapped.has(value)) { + return; + } else { + addedValueAsObjectsBeingMapped = true; + this._objectsBeingMapped.add(value); + } + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + inverseValue; + + if (propertyDescriptor.cardinality === 1) { + //value should not be an array + if (isArray(value)) { + console.warn( + "Something's off...., the value of propertyDescriptor:", + propertyDescriptor, + " of data object:", + dataObject, + " should not be an array" + ); + } + + if (value) { + if (inversePropertyCardinality > 1) { + /* + value needs to be added to the other's side: + + BUT - TODO - doing value[inversePropertyName] actually fires the trigger if wasn't there alredy. + In some cases, we rely on the value being there so it gets saved properly, by putting a foreignKey in for example. + It might be possible to handle that when we save only, or we could do the lookup using the property getter's secret shouldFetch argument. + + inverseValue = Object.getPropertyDescriptor(value,inversePropertyName).get(false); //<-shouldFetch false + + If we add the value and we don't know what was there (because we didn't fetch), we won't be able to do optimistic locking + We also would need to mark that property as "incommplete?", which we would need to do to able to add to a relationship without resolving it. + such that if the user actually fetch that property we can re-apply what was added/removed locally to what was actually fetched. + + Also value[inversePropertyName] does fire the trigger, but it's async, so we're likely missing the value here and we migh need to use a promise with + getObjectProperty/ies + + */ + inverseValue = value[inversePropertyName]; + if (inverseValue) { + /* + We might be looping back, but in any case, we shouldn't add the same object again, so we need to check if it is there. I really don't like doinf indexOf() here, but it's not a set... + */ + if (inverseValue.indexOf(dataObject) === -1) { + inverseValue.push(dataObject); + } + } else { + //No existing array so we create one on the fly + value[inversePropertyName] = [dataObject]; + } + } else { + /* + A 1-1 then. Let's not set if it's the same... + + CAVEAT: if inversePropertyName has not been fetched so far, we don't really know what the value is. + If the "join" is made on value's primary key, checking the snapshot wouldn't tell us anything. + If we do value[inversePropertyName], it does trigger a fetch anyway and the value returned may or may not + end up overriding value[inversePropertyName] = dataObject done here, which isn't what the user intended. + */ + setInversePromise = this.getObjectProperties(value, [inversePropertyName]).then(() => { + if (value[inversePropertyName] !== dataObject) { + value[inversePropertyName] = dataObject; + } + }); + } + } + + if (previousValue) { + inverseValue = previousValue[inversePropertyName]; + if (inversePropertyCardinality > 1) { + /* + previousValue needs to be removed from the other's side: + */ + if (inverseValue) { + /* + Assuming it only exists once in the array as it should... + */ + inverseValue.delete(dataObject); + } + // else { + // //No existing array so nothing to do.... + // } + } else if (inverseValue === dataObject) { + /* + only if previousValue still points back to dataObject, do we sever the relationship + This checks allows to break a cycle of 1-1 updatimg each other one side moves on + */ + //A 1-1 then + previousValue[inversePropertyName] = null; + } + } + } else if (propertyDescriptor.cardinality > 1) { + //value should be an array + if (value && !isArray(value)) { + console.warn( + "Something's off...., the value of propertyDescriptor:", + propertyDescriptor, + " of data object:", + dataObject, + " should be an array" + ); + } + + this._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor + ); + + if (previousValue) { + this._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + previousValue, + inversePropertyDescriptor + ); + } + // for(var i=0, countI = value.length, iValue; (i 1) { + // //many to many: + // //value needs to be added to the other's side: + // inverseValue = value[inversePropertyName]; + // if(inverseValue) { + // inverseValue.push(dataObject) + // } else { + // //No existing array so we create one on the fly + // value[inversePropertyName] = [dataObject]; + // } + + // } else { + // //A many-to-one + // iValue[inversePropertyName] = dataObject; + // } + + // } + } + + //Cleanup: + if (addedValueAsObjectsBeingMapped) { + if (setInversePromise) { + setInversePromise.then(() => { + this._objectsBeingMapped.delete(value); + }); + } else { + this._objectsBeingMapped.delete(value); + } + } + }, + }, + + /** + * handles the propagation of a values added to a property's array value to it's inverse property, fka "addToBothSidesOfRelationship..." + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _addDataObjectPropertyDescriptorValuesForInversePropertyName: { + value: function (dataObject, propertyDescriptor, value, inversePropertyName) { + //Now set the inverse if any + if (inversePropertyName) { + var inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor; /* Sync */ + if (!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function ( + inversePropertyDescriptorResolved + ) { + self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptorResolved + ); + }); + } else { + this._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor + ); + return Promise.resolveTrue; + } + } + }, + }, + + _addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor: { + value: function ( + dataObject, + propertyDescriptor, + values, + inversePropertyDescriptor, + isDataObjectBeingMapped + ) { + if (inversePropertyDescriptor) { + //value should be an array + if ((values && !isArray(values)) || !(propertyDescriptor.cardinality > 0)) { + console.warn( + "Something's off...., values added to propertyDescriptor:", + propertyDescriptor, + " of data object:", + dataObject, + " should be an array" + ); + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + i, + countI; + + for (i = 0, countI = values?.length; i < countI; i++) { + this._addDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + values[i], + inversePropertyDescriptor, + inversePropertyCardinality, + inversePropertyName, + isDataObjectBeingMapped + ); + } + } + }, + }, + + _addDataObjectPropertyDescriptorValueForInversePropertyDescriptor: { + value: function ( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor, + _inversePropertyCardinality, + _inversePropertyName, + isDataObjectBeingMapped + ) { + if (inversePropertyDescriptor && value) { + if (isDataObjectBeingMapped) { + this._objectsBeingMapped.add(value); + } + + if ((_inversePropertyCardinality || inversePropertyDescriptor.cardinality) > 1) { + //many to many: + //value, if there is one, needs to be added to the other's side: + inverseValue = value[_inversePropertyName || inversePropertyDescriptor.name]; + if (inverseValue) { + /* + We shouldn't add the same object again, so we need to check if it is there. I really don't like doinf indexOf() here, but it's not a set... + */ + if (inverseValue.indexOf(dataObject) === -1) { + inverseValue.push(dataObject); + } + } else { + //No existing array so we create one on the fly + value[_inversePropertyName || inversePropertyDescriptor.name] = [dataObject]; + } + } else { + //A many-to-one + let propertyName = _inversePropertyName || inversePropertyDescriptor.name, + objectPropertyValue = Object.getPropertyDescriptor(value, propertyName)?.get?.call( + value, + /*shouldFetch*/ false + ); + + if (objectPropertyValue !== dataObject) { + value[_inversePropertyName || inversePropertyDescriptor.name] = dataObject; + } + } + + if (isDataObjectBeingMapped) { + this._objectsBeingMapped.delete(value); + } + } + }, + }, + + /** + * handles the propagation of a values added to a property's array value to it's inverse property, fka "addToBothSidesOfRelationship..." + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _removeDataObjectPropertyDescriptorValuesForInversePropertyName: { + value: function (dataObject, propertyDescriptor, value, inversePropertyName) { + //Now set the inverse if any + if (inversePropertyName) { + var inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor; /* Sync */ + if (!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function ( + inversePropertyDescriptorResolved + ) { + self._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptorResolved + ); + }); + } else { + this._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor + ); + return Promise.resolveTrue; + } + } + }, + }, + + _removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor: { + value: function (dataObject, propertyDescriptor, values, inversePropertyDescriptor) { + if (inversePropertyDescriptor) { + //value should be an array + if (!isArray(values) || !(propertyDescriptor.cardinality > 0)) { + console.warn( + "Something's off...., values added to propertyDescriptor:", + propertyDescriptor, + " of data object:", + dataObject, + " should be an array" + ); + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + i, + countI; + + for (i = 0, countI = values.length; i < countI; i++) { + this._removeDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + values[i], + inversePropertyDescriptor, + inversePropertyCardinality, + inversePropertyName + ); + } + } + }, + }, + + _removeDataObjectPropertyDescriptorValueForInversePropertyDescriptor: { + value: function ( + dataObject, + propertyDescriptor, + value, + inversePropertyDescriptor, + _inversePropertyCardinality, + _inversePropertyName + ) { + if (inversePropertyDescriptor && value) { + if ((_inversePropertyCardinality || inversePropertyDescriptor.cardinality) > 1) { + /* + many to many: + value needs to be renoved to the other's side, unless it doesn't exists (which would be the case if it wasn't fetched). + */ + inverseValue = value[_inversePropertyName || inversePropertyDescriptor.name]; + if (inverseValue) { + inverseValue.delete(dataObject); + } + } else { + //A many-to-one, sever the ties + value[_inversePropertyName || inversePropertyDescriptor.name] = null; + } + } + }, + }, + + debouncedQueueMicrotaskWithDelay: { + value: queueMicrotask.debounceWithDelay(500), + }, + + registerDataObjectChangesFromEvent: { + value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { + var dataObject = changeEvent.target, + key = changeEvent.key, + objectDescriptor = this.objectDescriptorForObject(dataObject), + propertyDescriptor = objectDescriptor.propertyDescriptorForName(key), + isDataObjectBeingMapped = this._objectsBeingMapped.has(dataObject); + + // if(key === "jobRoles") { + // console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + " " +this.identifier+" registerDataObjectChangesFromEvent for primary key "+dataObject.dataIdentifier.primaryKey+ ", "+key); + // } + + //Property with definitions are read-only shortcuts, we don't want to treat these as changes the raw layers will want to know about + if (propertyDescriptor.definition) { + return; + } + + if (!isDataObjectBeingMapped && this.autosaves /* && !this.isAutosaveScheduled*/) { + + // if(key === "jobRoles") { + // console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + " debouncedQueueMicrotaskWithDelay: " +this.identifier+" registerDataObjectChangesFromEvent for primary key "+dataObject.dataIdentifier.primaryKey+ ", "+key); + // } + + //this.isAutosaveScheduled = true; + this.debouncedQueueMicrotaskWithDelay(() => { + //console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + " debounced saveChanges: " +this.identifier+" registerDataObjectChangesFromEvent for primary key "+dataObject.dataIdentifier.primaryKey+ ", "+key); + + this.isAutosaveScheduled = false; + this.saveChanges(); + }); + } + + // if (!isDataObjectBeingMapped && this.autosaves && !this.isAutosaveScheduled) { + // this.isAutosaveScheduled = true; + // queueMicrotask(() => { + // this.isAutosaveScheduled = false; + // this.saveChanges(); + // }); + // } + + var inversePropertyName = propertyDescriptor.inversePropertyName, + inversePropertyDescriptor; + + if (inversePropertyName) { + inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor /* Sync */; + if (!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function (_inversePropertyDescriptor) { + if (!_inversePropertyDescriptor) { + console.error( + "objectDescriptor " + + objectDescriptor.name + + "'s propertyDescriptor " + + propertyDescriptor.name + + " declares an inverse property named " + + inversePropertyName + + " on objectDescriptor " + + propertyDescriptor._valueDescriptorReference.name + + ", no matching propertyDescriptor could be found on " + + propertyDescriptor._valueDescriptorReference.name + ); + } else { + self._registerDataObjectChangesFromEvent( + changeEvent, + propertyDescriptor, + _inversePropertyDescriptor, + shouldTrackChangesWhileBeingMapped + ); + } + }); + } else { + this._registerDataObjectChangesFromEvent( + changeEvent, + propertyDescriptor, + inversePropertyDescriptor, + shouldTrackChangesWhileBeingMapped + ); + } + } else { + this._registerDataObjectChangesFromEvent( + changeEvent, + propertyDescriptor, + inversePropertyDescriptor, + shouldTrackChangesWhileBeingMapped + ); + } + }, + }, + + _registerDataObjectChangesFromEvent: { + value: function ( + changeEvent, + propertyDescriptor, + inversePropertyDescriptor, + shouldTrackChangesWhileBeingMapped + ) { + var dataObject = changeEvent.target, + transaction = changeEvent.transaction, + isCreatedObject = this.isObjectCreated(dataObject), + key = changeEvent.key, + keyValue = changeEvent.keyValue, + addedValues = changeEvent.addedValues, + removedValues = changeEvent.removedValues, + isDataObjectBeingMapped = this._objectsBeingMapped.has(dataObject), + changesForDataObject = transaction ? transaction.changesForDataObject(dataObject): this.changesForDataObject(dataObject), + //WARNING TEST: THIS WAS REDEFINING THE PASSED ARGUMENT + //inversePropertyDescriptor, + self = this; + + /* + Benoit refactoring saveChanges: shouldn't we be able to know that if there are no changesForDataObject, as we create on, it would ve the only time we'd have to call: + + this.registerChangedDataObject(dataObject); + + ? + #TODO TEST!! + */ + // if (dataObject.objectDescriptor.name === "EmploymentPositionStaffing") { + // debugger; + // } + + if (!isCreatedObject && (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped)) { + //this.changedDataObjects.add(dataObject); + changeEvent.transaction + ? this.registerChangedDataObject(dataObject, changesForDataObject, transaction.objectDescriptors) + : this.registerChangedDataObject(dataObject); + } + + //Now handled in changesForDataObject + // if(!changesForDataObject) { + // changesForDataObject = new Map(); + // this.dataObjectChanges.set(dataObject,changesForDataObject); + // } + + /* + + TODO / WARNING / FIX: If an object's property that has not been fetched, mapped and assigned is accessed, it will be undefined and will trigger a fetch to get it. If the business logic then assumes it's not there and set a value synchronously, when the fetch comes back, we will have a value and the set will look like an update. + + This situation is poorly handled and should be made more robust, here and in DataTrigger. + + Should we look into the snapshot to help? Then map what's there first, and then compare before acting? + + var dataObjectSnapshot = this._getChildServiceForObject(dataObject)._snapshot.get(dataObject.dataIdentifier); + + Just because it's async, doesn't mean we couldn't get it right, since we can act after the sync code action and reconciliate the 2 sides. + + */ + + /* + While a single change Event should be able to model both a range change + equivalent of minus/plus and a related length property change at + the same time, a changeEvent from the perspective of tracking data changes + doesn't really care about length, or the array itself. The key of the changeEvent will be one of the target's and the added/removedValues would be from that property's array if it's one. + + Which means that data objects setters should keep track of an array + changing on the object itself, as well as mutation done to the array itself while modeling that object's relatioonship. + + Client side we're going to have partial views of a whole relationship + as we may not want to fetch everything at once if it's big. Which means + that even if we can track add / removes to a property's array, what we + may consider as an add / remove client side, may be a no-op while it reaches the server, and we may want to be able to tell the client about that specific fact. + + + */ + if ( + changeEvent.key && + typeof changeEvent.keyValue !== "undefined" && + key !== "length" && + /* new for blocking re-entrant */ changesForDataObject?.get(key) !== keyValue + ) { + if (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped) { + if(!changesForDataObject) { + changesForDataObject = (transaction ? transaction._buildChangesForDataObject(dataObject) : this._buildChangesForDataObject(dataObject)); + } + changesForDataObject.set(key, keyValue); + } + + //Now set the inverse if any + if (inversePropertyDescriptor) { + self._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + keyValue, + inversePropertyDescriptor, + changeEvent.previousKeyValue, + isDataObjectBeingMapped + ); + } + } + + //A change event could carry both a key/value change and addedValues/remove, like a splice, where the key would be "length" + + if ((addedValues && addedValues.length > 0) || (removedValues && removedValues.length > 0)) { + + /* + TODO: FIXME + if addedValues and removedValues contain the same objects in a different order, + there's a bug in a way the graph is updated that is not symetric and the underlaying property of the object that change + ends up empty. + We set the to-one inverse of the objects in the array to null, + which in turn, understands this as it shouldn't belong in the array of its inverse relationship, from which it is removed. + + This is wastefull when it's just a different order that has no consequence for the graph itself. + But the problem is that when we process the addedValues that should re-set things, there's a problem in logic that guards + against upading the graph forever, that's ends the cycle before the state has been fully processed. + + #WARNING #TODO #FIXME - THAT NEEDS TO BE FIXED! + + */ + + //If both array contain the same values, there's nothing to do from a relationship/graph management stand point + if (addedValues.isContentEqual(removedValues)) { + return; + } + + + //For key that can have add/remove the value of they key is an object + //that itself has two keys: addedValues and removedValues + //which value will be a set; + var manyChanges = changesForDataObject?.get(key), + isManyChangesArray = manyChanges && isArray(manyChanges), + i, + countI; + + //TODO The logic for tracking array changes uses the index at which the change occured a la handleRangeChange(plus, minus, index). + // The problem is that it assumes there was a single action taken at a single index within a save cycle. + //Example: foo.bar = [A, B, C, D, E, F, G]; + // + // foo.bar.splice(2, 2); Removes 2 items at index 2 so change event has index 2 + // foo.bar.push(H); Adds item to end of array so change event has index 5 + // foo.bar.unshift(Z); Adds item to beginning of array so change event has index 0 + // saveChanges(); + // + // The index captured in dataObjectChanges is the one from the first change event, so 2. + // The indices at which to make the 2nd and 3rd changes are lost. + + // if (!manyChanges) { + // manyChanges = new CollectionChanges(); + // changesForDataObject.set(key, manyChanges); + // } + + + if (isManyChangesArray && (isCreatedObject || isDataObjectBeingMapped)) { + if (removedValues) { + self._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + removedValues, + inversePropertyDescriptor, + isDataObjectBeingMapped + ); + } + if (addedValues) { + self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + addedValues, + inversePropertyDescriptor, + isDataObjectBeingMapped + ); + } + } else { + + if (!manyChanges) { + manyChanges = new CollectionChanges(); + if(!changesForDataObject) { + changesForDataObject = (transaction ? transaction._buildChangesForDataObject(dataObject) : this._buildChangesForDataObject(dataObject)); + } + changesForDataObject.set(key, manyChanges); + } + + + if (addedValues && isManyChangesArray && manyChanges.equals(addedValues)) { + manyChanges = new CollectionChanges(); + changesForDataObject.set(key, manyChanges); + } + manyChanges.trackChangeEvent(changeEvent); + + if (removedValues) { + registeredRemovedValues = manyChanges.removedValues; + + if (isDataObjectBeingMapped) { + let targetName = changeEvent.target instanceof ObjectDescriptor ? changeEvent.target.name : changeEvent.target.dataIdentifier?.typeName; + + console.warn(`[DataService] RemovedValue during mapping? ${targetName}.${changeEvent.key}`); + } + + + for (i = 0, countI = removedValues.length; i < countI; i++) { + if (!isDataObjectBeingMapped) { + registeredRemovedValues.add(removedValues[i]); + } + self._removeDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + removedValues[i], + inversePropertyDescriptor, + isDataObjectBeingMapped + ); + } + } + + if (addedValues) { + registeredAddedValues = manyChanges.addedValues; + + if (isDataObjectBeingMapped) { + let targetName = changeEvent.target instanceof ObjectDescriptor ? changeEvent.target.name : changeEvent.target.dataIdentifier?.typeName; + console.warn(`[DataService] Add value during mapping? ${targetName}.${changeEvent.key}`); + } + let shouldAdd = true; + for (i = 0, countI = addedValues.length; i < countI; i++) { + if (!isDataObjectBeingMapped) { + if (!registeredAddedValues.has(addedValues[i])) { + registeredAddedValues.add(addedValues[i]); + shouldAdd = true; + } else if (!addedValues[i]) { + shouldAdd = true; + } else { + shouldAdd = false; + } + } + if (shouldAdd) { + self._addDataObjectPropertyDescriptorValueForInversePropertyDescriptor( + dataObject, + propertyDescriptor, + addedValues[i], + inversePropertyDescriptor, + isDataObjectBeingMapped + ); + } + } + } + } + + return; + + } + }, + }, + + clearRegisteredChangesForDataObject: { + value: function (dataObject) { + this.dataObjectChanges.set(dataObject, null); + }, + }, + + /** + * A set of the data objects managed by this service or any other descendent + * of this service's [root service]{@link DataService#rootService} that have + * been set for deletion since that root service's data was last saved, or since the + * root service was created if that service's data hasn't been saved yet + * + * Since root services are responsible for tracking data objects, subclasses + * whose instances will not be root services should override this property + * to return their root service's value for it. + * + * @type {Set.} + */ + deletedDataObjects: { + get: function () { + if (this.isRootService) { + this._deletedDataObjects = this._deletedDataObjects || new Map(); + return this._deletedDataObjects; + } else { + return this.rootService.deletedDataObjects; + } + }, + }, + + _deletedDataObjects: { + value: undefined, + }, + + registerDeletedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + deletedDataObjects = this.deletedDataObjects, + value = deletedDataObjects.get(objectDescriptor); + if (!value) { + deletedDataObjects.set(objectDescriptor, (value = new Set())); + } + value.add(dataObject); + this.objectDescriptorsWithChanges.add(objectDescriptor); + }, + }, + + isObjectDeleted: { + value: function (dataObject) { + return this.deletedDataObjects.get(dataObject.objectDescriptor)?.has(dataObject); + }, + }, + + unregisterDeletedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + value = this.deletedDataObjects.get(objectDescriptor); + if (value) { + value.delete(dataObject); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + }, + }, + + // handleBatchCompletedOperation: { + // value: function (operation) { + // this.handleOperationCompleted(operation); + // } + // }, + // handleBatchFailedOperation: { + // value: function (operation) { + // this.handleOperationFailed(operation); + // } + // }, + // handleCommitTransactionCompletedOperation: { + // value: function (operation) { + // this.handleOperationCompleted(operation); + // } + // }, + // handleCommitTransactionFailedOperation: { + // value: function (operation) { + // this.handleOperationFailed(operation); + // } + // }, + // handleRollbackTransactionCompletedOperation: { + // value: function (operation) { + // this.handleOperationCompleted(operation); + // } + // }, + // handleRollbackTransactionFailedOperation: { + // value: function (operation) { + // this.handleOperationFailed(operation); + // } + // }, + + /** + * Asynchronously validates a collection of data instances and attaches the + * resulting invalidity state to each instance as a non-enumerable property. + * + * @param {Map>} objects - A map where each value is a Set of objects to evaluate. + * @returns {Promise>>} A promise that resolves with a map where each + * key is an object and its value is its own invalidity state. + */ + _buildInvalidityStateForObjects: { + value: function (objects) { + const validationPromises = []; + + for (const dataObjects of objects.values()) { + for (const dataObject of dataObjects) { + const promise = this._getInvalidityStateForObject(dataObject).then((invalidityState) => { + this._setInvalidityStateOnObject(dataObject, invalidityState); + return [dataObject, invalidityState]; + }); + + validationPromises.push(promise); + } + } + + return Promise.all(validationPromises).then((resolvedPairs) => { + return new Map(resolvedPairs); + }); + }, + }, + + /** + * Asynchronously evaluates the validity of a single object and groups any errors by rule name. + * + * @param {object} object - The object to evaluate. + * @returns {Promise>} A promise that resolves with the object's + * invalidity state. An empty map signifies a valid object. + */ + _getInvalidityStateForObject: { + value: function (object) { + const invalidityState = new Map(); + const objectDescriptor = this.objectDescriptorForObject(object); + + return objectDescriptor + .evaluateObjectValidity(object) + .then((validationErrors) => { + // Group validation errors by rule name. + for (let i = 0, length = validationErrors.length; i < length; i++) { + const validationError = validationErrors[i]; + const rule = validationError.rule; + // A single rule can be associated with multiple properties. + const associatedProperties = rule.validationProperties || []; + + for (let j = 0, propsLength = associatedProperties.length; j < propsLength; j++) { + const propertyName = associatedProperties[j]; + + if (!invalidityState.has(propertyName)) { + invalidityState.set(propertyName, []); + } + + invalidityState.get(propertyName).push(validationError); + } + } + + return invalidityState; + }) + .catch((error) => { + // Log the error for debugging but re-throw to let the caller handle it. + console.error(`Error evaluating validity for object:`, object, error); + throw error; + }); + }, + }, + + /** + * Attaches an invalidity state to a data instance as a non-enumerable property. + * @param {object} object - The data instance to which the invalidity state will be attached. + * @param {Map} invalidityState - The invalidity state to attach. + * @returns {object} The modified object with the attached invalidity state. + */ + _setInvalidityStateOnObject: { + value: function (object, invalidityState) { + Object.defineProperty(object, "invalidityState", { + value: invalidityState, + writable: true, + configurable: true, + enumerable: false, + }); + + return object; + }, + }, + + _promisesByPendingTransactions: { + value: undefined, + }, + registerPendingTransactionPromise: { + value: function (transaction, promise) { + (this._promisesByPendingTransactions || (this._promisesByPendingTransactions = new Map())).set( + transaction, + promise + ); + }, + }, + registeredPromiseForPendingTransaction: { + value: function (transaction) { + return this._promisesByPendingTransactions?.get(transaction); + }, + }, + + hasPendingTransaction: { + get: function () { + return this._promisesByPendingTransactions && this._promisesByPendingTransactions.size > 0; + }, + }, + + pendingTransactions: { + get: function () { + return Array.from(this._promisesByPendingTransactions?.keys()); + }, + }, + pendingTransactionPromises: { + get: function () { + return Array.from(this._promisesByPendingTransactions?.values()); + }, + }, + + unregisterPendingTransactionPromise: { + value: function (transaction) { + if (this._promisesByPendingTransactions) { + this._promisesByPendingTransactions.delete(transaction); + } + }, + }, + + _pendingTransactions: { + value: undefined, + }, + addPendingTransaction: { + value: function (aCreateTransactionOperation) { + (this._pendingTransactions || (this._pendingTransactions = [])).push(aCreateTransactionOperation); + }, + }, + removePendingTransaction: { + value: function (aCreateTransactionOperation) { + if (this._pendingTransactions) { + this._pendingTransactions.delete(aCreateTransactionOperation); + } + }, + }, + + _dispatchTransactionEventTypeWithObjects: { + value: function (transaction, eventType, objects) { + var criteriaIterator = objects.keys(), + iteration, + iObjectDescriptor, + iObjects, + iTransactionEvent, + propagationPromises, + propagationPromise; + + /* + dispatch transactionCreate() + */ + while (!(iteration = criteriaIterator.next()).done) { + iObjectDescriptor = iteration.value; + iObjects = objects.get(iObjectDescriptor); + + iTransactionEvent = TransactionEvent.checkout(); + + iTransactionEvent.type = eventType; + iTransactionEvent.transaction = transaction; + iTransactionEvent.data = iObjects; + + iObjectDescriptor.dispatchEvent(iTransactionEvent); + propagationPromise = dataEvent.propagationPromise; + if (Promise.is(propagationPromise)) { + (propagationPromises || (propagationPromises = [])).push(propagationPromise); + propagationPromise.then(function () { + eventPool.checkin(dataEvent); + }); + } else { + eventPool.checkin(dataEvent); + } + } + + return propagationPromises ? Promise.all(propagationPromises) : null; + }, + }, + + /** + * + * Prepare. + * + */ + handleTransactionCreateStart: { + value: function (transactionPrepareStartEvent) { + var preparingParticipant = transactionPrepareStartEvent.target, + handledObjectDescriptors = transactionPrepareStartEvent.data; + + /* + TODO Future: use handledObjectDescriptors: + objectDescriptor -> Map { + "createdDatabjects" -> Set, + "changedDatabjects" -> Set, + "deletedDatabjects" -> Set + } + + along with handleTransationPrepareProgress() to track progress + */ + + //transactionPrepareStartEvent.transaction.createCompletionPromiseForParticipant(preparingParticipant); + + /* + listen for both complete and fail + */ + preparingParticipant.addEventListener(TransactionEvent.transationPrepareProgress, this, false); + preparingParticipant.addEventListener(TransactionEvent.transationPrepareComplete, this, false); + }, + }, + + /** + * + * Prepare. + * + */ + + handleTransactionPrepareStart: { + value: function (transactionPrepareStartEvent) { + var preparingParticipant = transactionPrepareStartEvent.target, + handledObjectDescriptors = transactionPrepareStartEvent.data; + + /* + TODO Future: use handledObjectDescriptors: + objectDescriptor -> Map { + "createdDatabjects" -> Set, + "changedDatabjects" -> Set, + "deletedDatabjects" -> Set + } + + along with handleTransationPrepareProgress() to track progress + */ + + //transactionPrepareStartEvent.transaction.createCompletionPromiseForParticipant(preparingParticipant); + + /* + listen for both complete and fail + */ + preparingParticipant.addEventListener(TransactionEvent.transationPrepareProgress, this, false); + preparingParticipant.addEventListener(TransactionEvent.transationPrepareComplete, this, false); + }, + }, + + handleTransactionPrepareProgress: { + value: function (transactionPrepareProgressEvent) { + var preparingParticipant = transactionPrepareProgressEvent.target; + //boilerplate for now + }, + }, + + handleTransactionPrepareComplete: { + value: function (transactionPrepareCompleteEvent) { + var participant = transactionPrepareCompleteEvent.target, + transaction = transactionPrepareCompleteEvent.transaction; + + //resolve the matching completionPromise with the participant. + //transaction.resolveCompletionPromiseForParticipant(participant); + }, + }, + + handleTransactionPrepareFail: { + value: function (transactionPrepareFailEvent) { + var participant = transactionPrepareFailEvent.target, + transaction = transactionPrepareFailEvent.transaction, + error = transactionPrepareFailEvent.data; + + //reject the matching completionPromise with the participant. + // transaction.rejectCompletionPromiseForParticipantWithError(participant, error); + }, + }, + + /** + * + * Cancel/abort + * + */ + + handleTransactionRollbackStart: { + value: function (transactionRollbackStartEvent) { + var participant = transactionRollbackStartEvent.target, + handledObjectDescriptors = participant.data; + + /* + TODO Future: use handledObjectDescriptors: + objectDescriptor -> Map { + "createdDatabjects" -> Set, + "changedDatabjects" -> Set, + "deletedDatabjects" -> Set + } + + along with handleTransationPrepareProgress() to track progress + */ + //transactionRollbackStartEvent.transaction.createCompletionPromiseForParticipant(participant); + + /* + listen for both complete and fail + */ + participant.addEventListener(TransactionEvent.transationCancelProgress, this, false); + participant.addEventListener(TransactionEvent.transationCancelComplete, this, false); + }, + }, + + handleTransactionRollbackProgress: { + value: function (transactionRollbackProgressEvent) { + var participant = transactionRollbackProgressEvent.target; + //boilerplate for now + }, + }, + + handleTransactionRollbackComplete: { + value: function (transactionRollbackCompleteEvent) { + var participant = transactionRollbackCompleteEvent.target, + transaction = transactionRollbackCompleteEvent.transaction; + + //resolve the matching completionPromise with the participant. + //transaction.resolveCompletionPromiseForParticipant(participant); + }, + }, + + handleTransactionRollbackFail: { + value: function (transactionRollbackFailEvent) { + var participant = transactionRollbackFailEvent.target, + transaction = transactionRollbackFailEvent.transaction, + error = transactionRollbackFailEvent.data; + + //reject the matching completionPromise with the participant. + //transaction.rejectCompletionPromiseForParticipantWithError(participant, error); + }, + }, + + /** + * + * Perform/commit. + * + */ + + handleTransactionCommitStart: { + value: function (transactionCommitStartEvent) { + var participant = transactionCommitStartEvent.target, + handledObjectDescriptors = participant.data; + + /* + TODO Future: use handledObjectDescriptors: + objectDescriptor -> Map { + "createdDatabjects" -> Set, + "changedDatabjects" -> Set, + "deletedDatabjects" -> Set + } + + along with handleTransationPrepareProgress() to track progress + */ + //transactionCommitStartEvent.transaction.createCompletionPromiseForParticipant(participant); + + /* + listen for both complete and fail + */ + participant.addEventListener(TransactionEvent.transationPerformProgress, this, false); + participant.addEventListener(TransactionEvent.transationPerformComplete, this, false); + }, + }, + + handleTransactionCommitProgress: { + value: function (transactionCommitProgressEvent) { + var participant = transactionCommitProgressEvent.target; + //boilerplate for now + }, + }, + + handleTransactionCommitComplete: { + value: function (transactionCommitCompleteEvent) { + var participant = transactionCommitCompleteEvent.target, + transaction = transactionCommitCompleteEvent.transaction; + + //resolve the matching completionPromise with the participant. + //transaction.resolveCompletionPromiseForParticipant(participant); + }, + }, + + handleTransactionCommitFail: { + value: function (transactionCommitFailEvent) { + var participant = transactionCommitFailEvent.target, + transaction = transactionCommitFailEvent.transaction, + error = transactionCommitFailEvent.data; + + //reject the matching completionPromise with the participant. + //transaction.rejectCompletionPromiseForParticipantWithError(participant, error); + }, + }, + + /* + * When it gets time to add/handle timeouts: + * + * https://advancedweb.hu/how-to-add-timeout-to-a-promise-in-javascript/ + */ + discardChanges: { + value: function () { + this.createdDataObjects.clear(); + this.changedDataObjects.clear(); + this.deletedDataObjects.clear(); + this.dataObjectChanges.clear(); + this.objectDescriptorsWithChanges.clear(); + }, + }, + + autosaves: { + value: false, + }, + + isAutosaveScheduled: { + value: false, + }, + saveChanges: { + value: function () { + return this.saveChangesWithIdentity(this.application.identity); + } + }, + saveChangesWithIdentity: { + value: function (identity) { + //If nothing to do, we bail out as early as possible. + if ( + this.createdDataObjects.size === 0 && + this.changedDataObjects.size === 0 && + this.deletedDataObjects.size === 0 + ) { + /* + If we have pending transation(s), then it means some logical saves got combined, so until we offer an API for intentional separation of changes, + best we can do is return a promise encompasing all pending. + + If anothe saveChanges were to be called, it may not use that same promise + */ + if (this.hasPendingTransaction) { + let pendingTransactionPromises = this.pendingTransactionPromises; + return pendingTransactionPromises.length === 1 + ? pendingTransactionPromises[0] + : Promise.all(this.pendingTransactionPromises); + } else { + var noOpOperation = new DataOperation(); + noOpOperation.type = DataOperation.Type.NoOp; + return Promise.resolve(noOpOperation); + } + } else { + if(this._pendingTransactions?.length > 0) { + console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + ">>>>>>>>> saveChangesWithIdentity called: #WARNING WITH _pendingTransactions! !!!!"); + } + } + + var transaction = new Transaction(), + self = this, + //Ideally, this should be saved in IndexedDB/PGLite so if something happen + //we can at least try to recover. + createdDataObjects = (transaction.createdDataObjects = new Map(this.createdDataObjects)), //Map + changedDataObjects = (transaction.updatedDataObjects = new Map(this.changedDataObjects)), //Map + deletedDataObjects = (transaction.deletedDataObjects = new Map(this.deletedDataObjects)), //Map + dataObjectChanges = (transaction.dataObjectChanges = new Map(this.dataObjectChanges)), //Map + objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( + this.objectDescriptorsWithChanges + )); + + /* + Properly set the transaction's identity property. + This could allow to implement an "super user" feature. Adding the ability for someone else + to authenticate, creating a second session with a different identity, to do something the current user + wouldn't be allowed to for example. + */ + transaction.identity = identity; + + + //console.log("saveChanges: transaction-"+this.identifier, transaction); + console.log( + "saveChanges: createdDataObjects [" + createdDataObjects.length + "]: ", + createdDataObjects, + "changedDataObjects [" + changedDataObjects.length + "]: ", + changedDataObjects, + "deletedDataObjects [" + deletedDataObjects.length + "]: ", + deletedDataObjects + ); + + // move to _saveChangesForTransaction() + //this.addPendingTransaction(transaction); + + // //We've made copies, so we clear right away to make room for a new cycle: + this.discardChanges(); + console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + ">>>>>>>>> saveChangesWithIdentity: #WARNING used to discardChanges() !!!!"); + + + if(this.pendingTransactionPromise) { + return this.pendingTransactionPromise.then(() => { + this.pendingTransactionPromise = this._saveChangesForTransaction(transaction); + return this.pendingTransactionPromise; + }) + } else { + this.pendingTransactionPromise = this._saveChangesForTransaction(transaction); + return this.pendingTransactionPromise; + } + }, + }, + + dispatchSaveEventTypeForTransaction: { + value: function(eventType, aTransaction) { + let transactionObjectDescriptors = aTransaction.objectDescriptors, + uniquePromise, + promises; + + for (const iObjectDescriptor of transactionObjectDescriptors) { + let iComposedPath = iObjectDescriptor.composedPath; + + /* + We need to make aTransaction part of the composedPath. + */ + iComposedPath = iComposedPath.slice(); + iComposedPath.splice(iComposedPath.indexOf(this), 0, aTransaction, this); + + let iPromise = this.dispatchDataEventTypeForObject( + eventType, + iObjectDescriptor, + aTransaction, + iComposedPath + ); + + if(iPromise) { + if(!promises) { + if(!uniquePromise) { + uniquePromise = iPromise; + } else { + promises = [uniquePromise, iPromise]; + uniquePromise = null; //not really needed + } + } else { + promises.push(iPromise) + } + } + } + + return promises + ? Promise.all(promises) + : uniquePromise + ? uniquePromise + : Promise.resolveUndefined; + + } + }, + + dispatchWillSaveForTransaction: { + value: function(aTransaction) { + return this.dispatchSaveEventTypeForTransaction(DataEvent.willSave, aTransaction) + } + }, + + _saveChangesForTransaction: { + value: function(transaction) { + let self = this; + + console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + ">>>>>>>>> _saveChangesForTransaction "+ transaction.identifier); + + this.addPendingTransaction(transaction); + + let pendingTransactionPromise = new Promise(function (resolve, reject) { + try { + var deletedDataObjectsIterator, + operation, + transactionObjectDescriptors = transaction.objectDescriptors, + iObject, + transactionPrepareEvent, + transactionCommitEvent, + createdDataObjects = transaction.createdDataObjects, //Map + changedDataObjects = transaction.updatedDataObjects, //Map + deletedDataObjects = transaction.deletedDataObjects; //Map + + // We first remove from create and update objects that are also deleted: + deletedDataObjectsIterator = deletedDataObjects.values(); + + while ((iObject = deletedDataObjectsIterator.next().value)) { + createdDataObjects.delete(iObject); + changedDataObjects.delete(iObject); + } + + // If nothing to do, we bail out + if ( + createdDataObjects.size === 0 && + changedDataObjects.size === 0 && + deletedDataObjects.size === 0 + ) { + operation = new DataOperation(); + operation.type = DataOperation.Type.NoOp; + // console.log("saveChanges: transaction-"+this.identifier+" <- NoOp", transaction); + resolve(operation); + } + + self.dispatchWillSaveForTransaction(transaction) + //Validate: + .then(() => { + + // //We've given a last chance to make changes, time to make room for a new cycle: + // console.debug(new Date().toLocaleTimeString([],{ hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) + ">>>>>>>>> saveChangesWithIdentity: #WARNING NEW discardChanges() !!!!"); + // self.discardChanges(); + + /** + * TODO: Refactor validation into operation-specific events. + * A single validation action is inefficient. We should split it by operation + * (e.g., `validateCreate`, `validateUpdate`, `validateDelete`) to run + * optimized, context-specific logic (like checking cascade-deletes only on delete). + */ + return Promise.all([ + // 1. Validate created objects. + self._buildInvalidityStateForObjects(createdDataObjects), + // 2. Validate updated objects. + self._buildInvalidityStateForObjects(changedDataObjects), + // 3. Validate deleted objects (e.g., check for rules that block deletion). + self._buildInvalidityStateForObjects(deletedDataObjects), + ]); + }) + //Dispatch Invalidity if any: + .then((validationResults) => { + const aggregatedValidationErrors = new Map(); + + // Merge validation results from all operations. (created, changed, deleted) + for (const result of validationResults) { + if (!result) continue; + + for (const [dataObject, invalidity] of result) { + // Only add if there are actual validation errors + if (invalidity && invalidity.size > 0) { + aggregatedValidationErrors.set(dataObject, invalidity); + + // Dispatch invalid data event for each invalid object + this.dispatchDataEventTypeForObject( + DataEvent.invalid, + dataObject, + invalidity + ); + } + } + } + + if (aggregatedValidationErrors.size > 0) { + const validateFailedOperation = new DataOperation(); + validateFailedOperation.type = DataOperation.Type.ValidateFailedOperation; + validateFailedOperation.target = self.mainService; + + // Pass the complete set of errors + validateFailedOperation.data = aggregatedValidationErrors; + + // Stop the transaction and resolve with the failure operation + resolve(validateFailedOperation); + + // TODO: @benoit should we not stop/reject the inner promise ? + return null; + } + + return transactionObjectDescriptors; + }, reject) + //Assess potential dependency to other pending transactions + .then(function (_transactionObjectDescriptors) { + + //Before we can get to it, we need to verify that "transaction"'s content has no dependency to a pending transaction + //for example an update to an object that's created in a pending transaction that has not completed yet, which can lead to deadlocks. + //We're going to start by handling that use-case only + + + + let pendingTransactions = self._pendingTransactions; + + if (pendingTransactions && pendingTransactions.length) { + + let updatedDataObjectsByObjectDescriptor = transaction.updatedDataObjects, //Map + updatedDataObjectDescriptorsIterator = updatedDataObjectsByObjectDescriptor.keys(), + firstPendingTransactionsCreatingChangedDataObjectsPromise, + pendingTransactionsCreatingChangedDataObjectsPromises, + iObjectDescriptor; + + /* + TODO WIP + Nested loop, really need to be optimized as we build up the transactions, + but let's get to work first and then we'll optimize. + + There can only be one pending transaction with the creation of iObject + so we bail out if we find it + */ + + //Loop on ObjectDescriptors with instances being updated + while ((iObjectDescriptor = updatedDataObjectDescriptorsIterator.next().value)) { + let updatedDataObjects = updatedDataObjectsByObjectDescriptor.get(iObjectDescriptor), + updatedDataObjectsIterator = updatedDataObjects.values(), + iObject; + + while ((iObject = updatedDataObjectsIterator.next().value)) { + + for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { + let iPendingTransaction = pendingTransactions[i]; + + if (iPendingTransaction.createdDataObjects.has(iObjectDescriptor)) { + let createdDataObjects = iPendingTransaction.createdDataObjects.get(iObjectDescriptor); + + if(createdDataObjects.has(iObject)) { + + if(!firstPendingTransactionsCreatingChangedDataObjectsPromise) { + firstPendingTransactionsCreatingChangedDataObjectsPromise = self.registeredPromiseForPendingTransaction(iPendingTransaction); + } else { + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + pendingTransactionsCreatingChangedDataObjectsPromises = new Set(); + pendingTransactionsCreatingChangedDataObjectsPromises.add(firstPendingTransactionsCreatingChangedDataObjectsPromise); + } + pendingTransactionsCreatingChangedDataObjectsPromises.add(self.registeredPromiseForPendingTransaction(iPendingTransaction)); + } + break; + } + + } + } + } + + } + + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + if(firstPendingTransactionsCreatingChangedDataObjectsPromise) { + return firstPendingTransactionsCreatingChangedDataObjectsPromise; + } else { + return Promise.resolveUndefined; + } + } else { + return Promise.all(Array.from(pendingTransactionsCreatingChangedDataObjectsPromises)) + } + } else { + return Promise.resolveUndefined + } + }) + //Dispatch TransactionEvent.transactionCreate + .then(function () { + var operationCount = + createdDataObjects.size + changedDataObjects.size + deletedDataObjects.size, + currentPropagationPromise, + operationIndex = 0, + iterator, + propagationPromises = []; + + /* + Now that we passed validation, and handling transaction dependencies, we're going to start the transaction + */ + + /* + Make sure we listen on ourselve (as events from RawDataServices will bubble to main) + for "transactionPrepareStart" + */ + //addEventListener(TransactionEvent.transactionCreateStart, self, false); + + //console.log("saveChanges: dispatchEvent transactionCreateEvent transaction-"+this.identifier, transaction); + + transactionCreateEvent = TransactionEvent.checkout(); + + transactionCreateEvent.type = TransactionEvent.transactionCreate; + transactionCreateEvent.transaction = transaction; + TransactionDescriptor.dispatchEvent(transactionCreateEvent); + + return transactionCreateEvent.propagationPromise || Promise.resolve(); + }) + //TransactionEvent.transactionCreate propagation completed + .then(function () { + return transaction.completionPromise; + }) + //Dispatch TransactionEvent.transactionPrepare + .then(function () { + /* + Make sure we listen on ourselve (as events from RawDataServices will bubble to main) + for "transactionPrepareStart" + */ + self.addEventListener(TransactionEvent.transactionPrepareStart, self, false); + + /* + Now dispatch transactionPrepare: + + Listened to by RawDataServices to know if they should be involved in dealing with transaction. + */ + transactionPrepareEvent = TransactionEvent.checkout(); + + transactionPrepareEvent.type = TransactionEvent.transactionPrepare; + transactionPrepareEvent.transaction = transaction; + + //console.log("saveChanges: dispatchEvent transactionPrepareEvent transaction-"+this.identifier, transaction); + + TransactionDescriptor.dispatchEvent(transactionPrepareEvent); + + return transactionPrepareEvent.propagationPromise || Promise.resolve(); + }) + //TransactionEvent.transactionPrepare propagation completed + .then(function () { + /* + Return the event to the pool + */ + TransactionEvent.checkin(transactionPrepareEvent); + return transaction.completionPromise; + }) + //Dispatch TransactionEvent.transactionCommit + .then(function () { + /* + Now all RawDataServices involved created the data operation they needed. + + time to dispatch transactionCommit: Once that's done, we wait for completionPromise + */ + + /* + Make sure we listen on ourselve (as events from RawDataServices will bubble to main) + for "transactionPrepareStart" + */ + self.addEventListener(TransactionEvent.transactionCommitStart, self, false); + + transactionCommitEvent = TransactionEvent.checkout(); + + transactionCommitEvent.type = TransactionEvent.transactionCommit; + transactionCommitEvent.transaction = transaction; + + //console.log("saveChanges: dispatchEvent transactionCommitEvent transaction-"+this.identifier, transaction); + + TransactionDescriptor.dispatchEvent(transactionCommitEvent); + + return transactionCommitEvent.propagationPromise + ? transactionCommitEvent.propagationPromise + : Promise.resolve(true); + }) + //TransactionEvent.transactionCommit propagation completed + .then(function () { + /* + Now transactionCommit has been dispatched to all listeners. + + transaction.participantCompletionPromises contains the final list of promises for when listeners will be done preaparing for the transation. + + We make a Promise.all() of it and wait to proceed or fail + */ + + /* + Return the event to the pool + */ + TransactionEvent.checkin(transactionCommitEvent); + + return transaction.completionPromise; + }) + .then(function () { + /* + If we're here, the transaction succeeded, all listeners successfully completed their work. + */ + // dataOperationsByObject = transaction.dataOperationsByObject; + + // self.didCreateDataObjects(createdDataObjects, dataOperationsByObject); + // self.didUpdateDataObjects(changedDataObjects, dataOperationsByObject); + // self.didDeleteDataObjects(deletedDataObjects, dataOperationsByObject); + + /* + We could also add an event to advertise teh result, but the transaction object already has everything. + */ + + //console.log("saveChanges: done! transaction-"+this.identifier, transaction); + self.removePendingTransaction(transaction); + self.unregisterPendingTransactionPromise(transaction); + + self.dispatchDidSaveForTransaction(transaction) + .then(() => { + resolve(transaction); + }); + }) + .catch(function (error) { + /* + TODO:... + At least one listener (typically a RawDataService) failed to execute transactionCommit/Commit, we have to return to a stable state.... if possible and finish the process. + + /* + At least one listener (typically a RawDataService) failed to prepare, we have to abort and return to a stable state.... + + There was a problem executing transactionCreate, at least one RawDataService couldn't create a transaction. DB may not be reachable? Operation may have been refused for lack of authorization? + + We still need to tell everyone it's off. + + */ + //console.log("saveChanges: CANCEL transaction-"+this.identifier, error, transaction); + + /* + If we're here, the transaction failed + */ + return self._cancelTransaction(transaction, error, reject); + }); + } catch (error) { + console.error(error) + self._cancelTransaction(transaction, error, reject); + } + }); + + this.registerPendingTransactionPromise(transaction, pendingTransactionPromise); + + return pendingTransactionPromise; + + } + }, + + dispatchDidSaveForTransaction: { + value: function(aTransaction) { + return this.dispatchSaveEventTypeForTransaction(DataEvent.didSave, aTransaction) + } + }, + + pendingTransactionPromiseForObjects: { + value: function (objects) { + let pendingTransactions = this._pendingTransactions; + + if (pendingTransactions && pendingTransactions.length) { + + let firstPendingTransactionsCreatingChangedDataObjectsPromise, + pendingTransactionsCreatingChangedDataObjectsPromises; + + /* + TODO WIP + Nested loop, really need to be optimized as we build up the transactions, + but let's get to work first and then we'll optimize. + + There can only be one pending transaction with the creation of iObject + so we bail out if we find it + */ + + //Loop on ObjectDescriptors with instances being updated + + for (let j = 0, countJ = objects.length; j < countJ; j++) { + let object = objects[j]; + + for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { + let iPendingTransaction = pendingTransactions[i]; + + if (iPendingTransaction.createdDataObjects.has(object.objectDescriptor)) { + let createdDataObjects = iPendingTransaction.createdDataObjects.get(object.objectDescriptor); + + if(createdDataObjects.has(object)) { + + if(!firstPendingTransactionsCreatingChangedDataObjectsPromise) { + firstPendingTransactionsCreatingChangedDataObjectsPromise = this.registeredPromiseForPendingTransaction(iPendingTransaction); + } else { + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + pendingTransactionsCreatingChangedDataObjectsPromises = new Set(); + pendingTransactionsCreatingChangedDataObjectsPromises.add(firstPendingTransactionsCreatingChangedDataObjectsPromise); + } + pendingTransactionsCreatingChangedDataObjectsPromises.add(this.registeredPromiseForPendingTransaction(iPendingTransaction)); + } + break; + } + + } + } + } + if(!pendingTransactionsCreatingChangedDataObjectsPromises) { + if(firstPendingTransactionsCreatingChangedDataObjectsPromise) { + return firstPendingTransactionsCreatingChangedDataObjectsPromise; + } else { + return Promise.resolveUndefined; + } + } else { + return Promise.all(Array.from(pendingTransactionsCreatingChangedDataObjectsPromises)) + } + } else { + return Promise.resolveUndefined; + } + + + } + }, + + _cancelTransaction: { + value: function (transaction, cancelError, rejectFunction) { + this.addEventListener(TransactionEvent.transactionRollbackStart, this, false); + + transactionRollbackEvent = TransactionEvent.checkout(); + + transactionRollbackEvent.type = TransactionEvent.transactionRollback; + transactionRollbackEvent.transaction = transaction; + TransactionDescriptor.dispatchEvent(transactionRollbackEvent); + + return (transactionRollbackEvent.propagationPromise || Promise.resolve()) + .then(function () { + TransactionEvent.checkin(transactionRollbackEvent); + return transaction.completionPromise; + }) + .then(function () { + rejectFunction(cancelError); + }) + .catch(function (error) { + rejectFunction(error); + }) + .finally(() => { + this.unregisterPendingTransactionPromise(transaction); + }); + }, + }, + + /** + * supportsDataOperation + * + * #WARNING Backward compatibility breaking + * TODO: Plan to remove it once more tests are done with + * older code base + * + * @property {boolean} assess that a DataService supports supportsDataOperations. + * Legacy ones that don't need to overrides it to false to work as expected + */ + + supportsDataOperation: { + value: true, + }, + + // operationReferrer: { + // value: function(operation) { + // return this._pendingOperationById.get(operation.referrerId); + // } + // }, + + // registerPendingOperation: { + // value: function(operation, referrer) { + // this._pendingOperationById.set(operation.id, operation); + + // } + // }, + + // unregisterOperationReferrer: { + // value: function(operation) { + // this._pendingOperationById.delete(operation.referrerId); + // } + // }, + + // _operationListenerNamesByType: { + // value: new Map() + // }, + // _operationListenerNameForType: { + // value: function(type) { + // return this._operationListenerNamesByType.get(type) || this._operationListenerNamesByType.set(type,"handle"+type.toCapitalized()).get(type); + // } + // }, + + shouldAuthenticateReadOperation: { + value: false, + }, + + //This probably isn't right and should be fetchRawData, but switching creates a strange error. + // _fetchDataWithOperation: { + // value: function (query, stream) { + + // var self = this; + // stream = stream || new DataStream(); + // stream.query = query; + + // // make sure type is an object descriptor or a data object descriptor. + // // query.type = this.rootService.objectDescriptorForType(query.type); + + // var objectDescriptor = query.type, + // criteria = query.criteria, + // criteriaWithLocale, + // parameters, + // rawParameters, + // readOperation = new DataOperation(), + // rawReadExpressions = [], + // rawOrderings, + // promises; + // // localizableProperties = objectDescriptor.localizablePropertyDescriptors; + + // /* + // We need to turn this into a Read Operation. Difficulty is to turn the query's criteria into + // one that doesn't rely on objects. What we need to do before handing an operation over to another context + // bieng a worker on the client side or a worker on the server side, is to remove references to live objects. + // One way to do this is to replace every object in a criteria's parameters by it's data identifier. + // Another is to serialize the criteria. + // */ + // readOperation.type = DataOperation.Type.ReadOperation; + // readOperation.target = objectDescriptor; + // //readOperation.data = {}; + + // //Need to add a check to see if criteria may have more spefific instructions for "locale". + // /* + // 1/19/2021 - we were only adding locale when the object descriptor being fetched has some localizableProperties, but a criteria may involve a subgraph and we wou'd have to go through the syntactic tree of the criteria, and readExpressions, to figure out if anywhere in that subgraph, there might be localizable properties we need to include the locales for. + + // Since we're localized by default, we're going to include it no matter what, it's going to be more rare that it is not needed than it is. + // */ + // /* + // WIP Adds locale as needed. Most common case is that it's left to the framework to qualify what Locale to use. + + // A core principle is that each data object (DO) has a locale property behaving in the following way: + // locales has 1 locale value, a locale object. + // This is the most common use case. The property’s getter returns the user’s locale. + // Fetching an object with a criteria asking for a specific locale will return an object in that locale. + // Changing the locale property of an object to another locale instance (singleton in Locale’s case), updates all the values of its localizable properties to the new locale set. + // locales has either no value, or “*” equivalent, an “All Locale Locale” + // This feches the json structure and returns all the values in all the locales + // locales has an array of locale instances. + // If locale’s cardinality is > 1 then each localized property would return a json/dictionary of locale->value instead of 1 value. + // */ + + // readOperation.locales = self.userLocales; + + // if(criteria) { + // readOperation.criteria = criteria; + // } + + // if(query.sizeLimit) { + // readOperation.data.sizeLimit = query.sizeLimit; + // } + + // if(query.orderings && query.orderings > 0) { + // rawOrderings = []; + // // self._mapObjectDescriptorOrderingsToRawOrderings(objectDescriptor, query.sortderings,rawOrderings); + // // readOperation.data.orderings = rawOrderings; + // readOperation.data.orderings = query.orderings; + // } + + // /* + // for a read operation, we already have criteria, shouldn't data contains the array of + // expressions that are expected to be returned? + // */ + // /* + // The following block is from PhrontClientService, we shouldn't map to rawReadExpressions just yet. + // */ + // // self._mapObjectDescriptorReadExpressionToRawReadExpression(objectDescriptor, query.readExpressions,rawReadExpressions); + // // if(rawReadExpressions.length) { + // // readOperation.data.readExpressions = rawReadExpressions; + // // } + // if(query.readExpressions && query.readExpressions.length) { + // readOperation.data.readExpressions = query.readExpressions; + // } + + // /* + // We need to do this in node's DataWorker, it's likely that we'll want that client side as well, where it's some sort of token set post authorization. + // */ + // if(this.application.identity && this.shouldAuthenticateReadOperation) { + // readOperation.identity = this.application.identity; + // } + + // /* + + // this is half-assed, we're mapping full objects to RawData, but not the properties in the expression. + // phront-service does it, but we need to stop doing it half way there and the other half over there. + // SaveChanges is cleaner, but the job is also easier there. + + // */ + // parameters = criteria ? criteria.parameters : undefined; + // rawParameters = parameters; + + // if(parameters && typeof criteria.parameters === "object") { + // var keys = Object.keys(parameters), + // i, countI, iKey, iValue, iRecord; + + // rawParameters = Array.isArray(parameters) ? [] : {}; + + // for(i=0, countI = keys.length;(i < countI); i++) { + // iKey = keys[i]; + // iValue = parameters[iKey]; + // if(!iValue) { + // throw "fetchData: criteria with no value for parameter key "+iKey; + // } else { + // if(iValue.dataIdentifier) { + // /* + // this isn't working because it's causing triggers to fetch properties we don't have + // and somehow fails, but it's wastefull. Going back to just put primary key there. + // */ + // // iRecord = {}; + // // rawParameters[iKey] = iRecord; + // // (promises || (promises = [])).push( + // // self._mapObjectToRawData(iValue, iRecord) + // // ); + // rawParameters[iKey] = iValue.dataIdentifier.primaryKey; + // } else { + // rawParameters[iKey] = iValue; + // } + // } + + // } + // // if(promises) promises = Promise.all(promises); + // } + // // if(!promises) promises = Promise.resolve(true); + // // promises.then(function() { + // if(criteria) { + // readOperation.criteria.parameters = rawParameters; + // } + // //console.log("fetchData operation:",JSON.stringify(readOperation)); + // self._dispatchReadOperation(readOperation, stream); + + // if(criteria) { + // readOperation.criteria.parameters = parameters; + // } + + // // }); + + // return stream; + // } + // }, + + // _dispatchReadOperation: { + // value: function(operation, stream) { + // this._thenableByOperationId.set(operation.id, stream); + // this._dispatchOperation(operation); + // } + // }, + // _dispatchOperation: { + // value: function(operation) { + // this._pendingOperationById.set(operation.id, operation); + + // defaultEventManager.handleEvent(operation); + + // // var serializedOperation = this._serializer.serializeObject(operation); + + // // // if(operation.type === "batch") { + // // // var deserializer = new Deserializer(); + // // // deserializer.init(serializedOperation, require, undefined, module, true); + // // // var deserializedOperation = deserializer.deserializeObject(); + + // // // console.log(deserializedOperation); + + // // // } + // // console.log("----> send operation "+serializedOperation); + // // this._socket.send(serializedOperation); + // } + // }, + + // registeredDataStreamForDataOperation: { + // value: function(dataOperation) { + // return this._thenableByOperationId + // ? dataOperation.referrerId + // ? this._thenableByOperationId.get(dataOperation.referrerId) + // : this._thenableByOperationId.get(dataOperation.id) + // : undefined; + // } + // }, + // unregisterDataStreamForDataOperation: { + // value: function(dataOperation) { + // this._thenableByOperationId + // ? dataOperation.referrerId + // ? this._thenableByOperationId.delete(dataOperation.referrerId) + // : this._thenableByOperationId.delete(dataOperation.id) + // : undefined; + // } + // }, + + // handleReadUpdateOperation: { + // value: function (operation) { + // var referrer = operation.referrerId, + // objectDescriptor = operation.target, + // records = operation.data, + // stream = this._thenableByOperationId.get(referrer), + // streamObjectDescriptor; + // // if(operation.type === DataOperation.Type.ReadCompletedOperation) { + // // console.log("handleReadCompleted referrerId: ",operation.referrerId, "records.length: ",records.length); + // // } else { + // // console.log("handleReadUpdateOperation referrerId: ",operation.referrerId, "records.length: ",records.length); + // // } + // //if(operation.type === DataOperation.Type.ReadUpdateOperation) console.log("handleReadUpdateOperation referrerId: ",referrer); + + // if(stream) { + // streamObjectDescriptor = stream.query.type; + // /* + + // We now could get readUpdate that are reads for readExpressions that are properties (with a valueDescriptor) of the ObjectDescriptor of the referrer. So we need to add a check that the obectDescriptor maatch, otherwise, it needs to be assigned to the right instance, or created in memory and mapping/converters will find it. + // */ + + // if(streamObjectDescriptor === objectDescriptor) { + // if(records && records.length > 0) { + // //We pass the map key->index as context so we can leverage it to do record[index] to find key's values as returned by RDS Data API + // this.addRawData(stream, records, operation); + // } else if(operation.type !== DataOperation.Type.ReadCompletedOperation){ + // console.log("operation of type:"+operation.type+", has no data"); + // } + // } else { + // console.log("Received "+operation.type+" operation that is for a readExpression of referrer ",referrer); + // } + // } else { + // console.log("receiving operation of type:"+operation.type+", but can't find a matching stream"); + // } + // } + // }, + + // handleReadCompletedOperation: { + // value: function (operation) { + // this.handleReadUpdateOperation(operation); + // //The read is complete + // var stream = this._thenableByOperationId.get(operation.referrerId); + // if(stream) { + // this.rawDataDone(stream); + // this._thenableByOperationId.delete(operation.referrerId); + // } else { + // console.log("receiving operation of type:"+operation.type+", but can't find a matching stream"); + // } + // //console.log("handleReadCompleted -clear _thenableByOperationId- referrerId: ",operation.referrerId); + + // } + // }, + + handleReadFailedOperation: { + value: function (operation) { + var stream = this._thenableByOperationId.get(operation.referrerId); + if (stream) { + this.rawDataError(stream, operation.data); + this._thenableByOperationId.delete(operation.referrerId); + } + }, + }, + + // handleOperationCompleted: { + // value: function (operation) { + // var referrerOperation = this._pendingOperationById.get(operation.referrerId); + + // /* + // Right now, we listen for the types we care about, on the mainService, so we're receiving it all, + // even those from other data services / types we don' care about, like the PlummingIntakeDataService. + + // One solution is to, when we register the types in the data service, to test if it handles operations, and if it does, the add all listeners. But that's a lot of work which will slows down starting time. A better solution would be to do like what we do with Components, where we find all possibly interested based on DOM structure, and tell them to prepare for a first delivery of that type of event. We could do the same as we know which RawDataService handle what ObjectDescriptor, which would give the RawDataService the ability to addListener() right when it's about to be needed. + + // Another solution could involve different "pools" of objects/stack, but we'd lose the universal bus. + + // */ + // if(!referrerOperation) { + // return; + // } + + // /* + // After creation we need to do this: self.rootService.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + + // The referrerOperation could get hold of object, but it doesn't right now. + // We could also create a uuid client side and not have to do that and deal wih it all in here which might be cleaner. + + // Now resolving the promise finishes the job in saveObjectData that has the object in scope. + // */ + // referrerOperation._promiseResolve(operation); + // } + // }, + + // handleOperationFailed: { + // value: function (operation) { + // var referrerOperation = this._pendingOperationById.get(operation.referrerId); + + // /* + // After creation we need to do this: self.rootService.registerUniqueObjectWithDataIdentifier(object, dataIdentifier); + + // The referrerOperation could get hold of object, but it doesn't right now. + // We could also create a uuid client side and not have to do that and deal wih it all in here which might be cleaner. + + // Now resolving the promise finishes the job in saveObjectData that has the object in scope. + // */ + // if(referrerOperation) { + // referrerOperation._promiseResolve(operation); + // } + // } + // }, + + // handleCreateCompletedOperation: { + // value: function (operation) { + // this.handleOperationCompleted(operation); + // } + // }, + + // handleUpdateCompletedOperation: { + // value: function (operation) { + // this.handleOperationCompleted(operation); + // } + // }, + + /*************************************************************************** + * Fetching Data + */ + + /* + DataStream cache: + + Map: ObjectDescriptor -> Map: criteriaExpression -> Map: JSON.stringify(criteria.parameters) -> Promise + */ + _dataStreamByObjectDescriptorByCriteriaExpressionByCriteriaParameters: { + value: new Map(), + }, + + _dataStreamMapForObjectDescriptor: { + value: function (objectDescriptor) { + var map = + this._dataStreamByObjectDescriptorByCriteriaExpressionByCriteriaParameters.get(objectDescriptor); + if (!map) { + map = new Map(); + this._dataStreamByObjectDescriptorByCriteriaExpressionByCriteriaParameters.set( + objectDescriptor, + map + ); + } + return map; + }, + }, + + _dataStreamMapForObjectDescriptorCriteria: { + value: function (objectDescriptor, criteria) { + var objectDescriptorMap = this._dataStreamMapForObjectDescriptor(objectDescriptor), + criteriaExpressionMap = objectDescriptorMap.get(criteria.expression); + if (!criteriaExpressionMap) { + criteriaExpressionMap = new Map(); + objectDescriptorMap.set(criteria.expression, criteriaExpressionMap); + } + return criteriaExpressionMap; + }, + }, + + makeCriteriaParametersReplacer: { + value: function () { + let isInitial = true; + + return (key, value) => { + if (isInitial) { + isInitial = false; + return value; + } + if (key === "") { + // Omit all properties with name "" (except the initial object) + return undefined; + } + + return this._criteriaParametersReplacer(key, value); + }; + }, + }, + + _criteriaParametersReplacer: { + value: function (key, value) { + return typeof value === "object" + ? key === "" + ? JSON.stringify(value, this._criteriaParametersReplacer) + : value?.dataIdentifier + ? value.dataIdentifier + : !!value + ? value.toString() + : null + : isArray(value) + ? value.map(this._criteriaParametersReplacer) + : value; + }, + }, + + _criteriaExpressionMapKeyForDataQuery: { + value: function (dataQuery) { + return typeof (dataQuery.criteria || Criteria.thatEvaluatesToTrue).parameters === "string" + ? dataQuery.readExpressions + ? `${JSON.stringify(dataQuery.readExpressions)}-${ + (dataQuery.criteria || Criteria.thatEvaluatesToTrue).parameters + }` + : (dataQuery.criteria || Criteria.thatEvaluatesToTrue).parameters + : dataQuery.readExpressions + ? `${JSON.stringify(dataQuery.readExpressions)}-${JSON.stringify( + (dataQuery.criteria || Criteria.thatEvaluatesToTrue).parameters, + this.makeCriteriaParametersReplacer() + )}` + : JSON.stringify( + (dataQuery.criteria || Criteria.thatEvaluatesToTrue).parameters, + this.makeCriteriaParametersReplacer() + ); + }, + }, + + registeredDataStreamMapForDataQuery: { + value: function (dataQuery) { + var objectDescriptor = dataQuery.type, + criteria = dataQuery.criteria || Criteria.thatEvaluatesToTrue, + criteriaExpressionMap = this._dataStreamMapForObjectDescriptorCriteria(objectDescriptor, criteria); + + return criteriaExpressionMap.get(this._criteriaExpressionMapKeyForDataQuery(dataQuery)); + }, + }, + + registerDataStream: { + value: function (dataStream) { + var objectDescriptor = dataStream.query.type, + criteria = dataStream.query.criteria || Criteria.thatEvaluatesToTrue, + criteriaExpressionMap = this._dataStreamMapForObjectDescriptorCriteria(objectDescriptor, criteria); + + return criteriaExpressionMap.set( + this._criteriaExpressionMapKeyForDataQuery(dataStream.query), + dataStream + ); + }, + }, + unregisterDataStream: { + value: function (dataStream) { + var objectDescriptor = dataStream.query.type, + criteria = dataStream.query.criteria || Criteria.thatEvaluatesToTrue, + criteriaExpressionMap = this._dataStreamMapForObjectDescriptorCriteria(objectDescriptor, criteria); + + return criteriaExpressionMap.delete(this._criteriaExpressionMapKeyForDataQuery(dataStream.query)); + }, + }, + + /* + DataStream cache: + + Map: ObjectDescriptor -> Map: criteriaExpression -> Map: JSON.stringify(criteria.parameters) -> Promise + */ + __rawDataServiceHandlersByReadEventId: { + value: undefined, + }, + _rawDataServiceHandlersByReadEventId: { + get: function () { + return ( + this.__rawDataServiceHandlersByReadEventId || + (this.__rawDataServiceHandlersByReadEventId = new Map()) + ); + }, + }, + registerReadEvent: { + value: function (readEvent) { + this._rawDataServiceHandlersByReadEventId.set(readEvent.id, undefined); + }, + }, + + __rawDataServiceHandlersByReadOperation: { + value: undefined, + }, + _rawDataServiceHandlersByReadOperation: { + get: function () { + return ( + this.__rawDataServiceHandlersByReadOperation || + (this.__rawDataServiceHandlersByReadOperation = new Map()) + ); + }, + }, + + registerRawDataServiceForReadOperation: { + value: function (rawDataService, readOperation) { + let handlers = this._rawDataServiceHandlersByReadOperation.get(readOperation); + if (!handlers) { + this._rawDataServiceHandlersByReadOperation.set(readOperation, (handlers = [])); + } + handlers.push(rawDataService); + + //When readOperation's referrer was a readEvent + //this.registerRawDataServiceHandlerForReadEventId(rawDataService, readOperation.referrerId); + this.registerRawDataServiceHandlerForReadEventId(rawDataService, readOperation.id); + }, + }, + + registerRawDataServiceHandlerForReadEventId: { + value: function (rawDataService, readEventId) { + let handlers = this._rawDataServiceHandlersByReadEventId.get(readEventId); + if (!handlers) { + this._rawDataServiceHandlersByReadEventId.set(readEventId, (handlers = [])); + } + handlers.push(rawDataService); + }, + }, + unregisterRawDataServiceHandlerForReadEventId: { + value: function (rawDataService, readEventId) { + let handlers = this._rawDataServiceHandlersByReadEventId.get(readEventId); + if (handlers) { + handlers.delete(rawDataService); + } + }, + }, + hasRegisteredRawDataServiceHandlerForReadEvent: { + value: function (readEvent) { + let handlers = this._rawDataServiceHandlersByReadEventId.get(readEvent.id); + return handlers && handlers.length > 0; + }, + }, + unregisterReadEvent: { + value: function (readEvent) { + if (!this.hasRegisteredRawDataServiceHandlerForReadEvent(readEvent)) { + /* + This means no RawDataService were able to handle that read. + We need to report am error + */ + /* + REFACTOR: There could be more than one rawDataService handling a read operation. + + RawDataService in handleRead used to create a readDataOperation for itself, + and set itself as: + readOperation.rawDataService = this; + Now in captureReadOperation(), it's missing... + + */ + //readEvent.dataStream.dataError(new Error("No RawDataService to handle query for "+readEvent.dataStream.query.type.name)); + } + + this._rawDataServiceHandlersByReadEventId.delete(readEvent.id); + ReadEvent.checkin(readEvent); + }, + }, + + /** + * Fetch data from the service using its child services. + * + * This method accept [types]{@link DataObjectDescriptor} as alternatives to + * [queries]{@link DataQuery}, and its [stream]{DataStream} argument is + * optional, but when it calls its child services it will provide them with + * a [query]{@link DataQuery}, it provide them with a + * [stream]{DataStream}, creating one if necessary, and the stream will + * include a reference to the query. Also, if a child service's + * implementation of this method return `undefined` or `null`, this method + * will return the stream passed in to the call to that child. + * + * The requested data may be fetched asynchronously, in which case the data + * stream will be returned immediately but the stream's data will be added + * to the stream at a later time. + * + * @method + * @argument {DataQuery|DataObjectDescriptor|ObjectDescriptor|Function|String} + * queryOrType - If this argument's value is a query + * it will define what type of data should + * be returned and what criteria that data + * should satisfy. If the value is a type + * it will only define what type of data + * should be returned, and the criteria + * that data should satisfy can be defined + * using the `criteria` argument. A type + * is defined as either a DataObjectDesc- + * riptor, an Object Descriptor, a Construct- + * or the string module id. The method will + * convert the passed in type to a Data- + * ObjectDescriptor (deprecated) or an + * ObjectDescriptor. This is true whether + * passing in a DataQuery or a type. + * @argument {?Object} + * optionalCriteria - If the first argument's value is a + * type this argument can optionally be + * provided to defines the criteria which + * the returned data should satisfy. + * If the first argument's value is a + * query this argument should be + * omitted and will be ignored if it is + * provided. + * @argument {?DataStream} + * optionalStream - The stream to which the provided data + * should be added. If no stream is + * provided a stream will be created and + * returned by this method. + * @returns {?DataStream} - The stream to which the fetched data objects + * were or will be added, whether this stream was provided to or created by + * this method. + */ + fetchData: { + value: function (queryOrType, optionalCriteria, optionalStream) { + if (!queryOrType) { + return Promise.resolveNull; + } + + var self = this, + isSupportedType = !(queryOrType instanceof DataQuery), + type = isSupportedType && queryOrType, + criteria = optionalCriteria instanceof DataStream ? undefined : optionalCriteria, + query = type ? DataQuery.withTypeAndCriteria(type, criteria) : queryOrType, + stream = optionalCriteria instanceof DataStream ? optionalCriteria : optionalStream; + + // make sure type is an object descriptor or a data object descriptor. + query.type = this.objectDescriptorForType(query.type); + + if (this.supportsDataOperation) { + //Check if we already have a DataStream pending for that same query: + if ((stream = this.registeredDataStreamMapForDataQuery(query))) { + //console.debug("registeredDataStreamMapForDataQuery found for "+query.type.name+", criteria:",query.criteria); + return stream; + } + } + + // Set up the stream. + stream = stream || new DataStream(); + stream.query = query; + stream.dataExpression = query.selectExpression; + + //Now before going further, quick check to see if we may have local read-only objects fitting the bill + let readOnlyDataObjectsRegisteredForObjectDescriptor = + this.readOnlyDataObjectsRegisteredForObjectDescriptor(query.type); + if (readOnlyDataObjectsRegisteredForObjectDescriptor) { + let result = readOnlyDataObjectsRegisteredForObjectDescriptor.filter(criteria.predicateFunction); + if (result && result.length > 0) { + stream.addData(result); + + let dataDoneTimeputId = setTimeout(() => { + clearTimeout(dataDoneTimeputId); + dataDoneTimeputId = null; + stream.dataDone(); + }, 0); + } + } + + //Register it for possible reuse by other callers needing the same data: + this.registerDataStream(stream); + + this._dataServiceByDataStream.set( + stream, + this._childServiceRegistrationPromise.then(function () { + /* + This a switch for 0% operations vs 100% operations. It should be possible to mix but it's more risky for side effects in older versions. + + In early versions of montage data, Data Objects and their Object Descriptors did not inherit from Target and therefore didn't respond to the .dispatchEvent() method. So we can use that to for the logic between new and old. + + supportsDataOperation will be determined by all RawDataServices supportsDataOperation + */ + + if (self.supportsDataOperation && query.type.dispatchEvent) { + // try { + + // var readEvent = ReadEvent.checkout(); + + // readEvent.type = ReadEvent.read; + // readEvent.query = query; + // readEvent.dataStream = stream; + + // self.registerReadEvent(readEvent); + + // query.type.dispatchEvent(readEvent); + // if(readEvent.propagationPromise) { + // readEvent.propagationPromise.finally(() => { + // self.unregisterReadEvent(readEvent); + // }); + // } else { + // self.unregisterReadEvent(readEvent); + // } + // } catch (e) { + // stream.dataError(e); + // } + + try { + var readOperation = new DataOperation(); + + readOperation.type = DataOperation.Type.ReadOperation; + readOperation.target = query.type; + + if (query.identity) { + readOperation.identity = query.identity; + } else if (self.identity) { + readOperation.identity = self.identity; + } + + //Need to add a check to see if criteria may have more spefific instructions for "locale". + /* + 1/19/2021 - we were only adding locale when the object descriptor being fetched has some localizableProperties, but a criteria may involve a subgraph and we wou'd have to go through the syntactic tree of the criteria, and readExpressions, to figure out if anywhere in that subgraph, there might be localizable properties we need to include the locales for. + + Since we're localized by default, we're going to include it no matter what, it's going to be more rare that it is not needed than it is. + */ + /* + WIP Adds locale as needed. Most common case is that it's left to the framework to qualify what Locale to use. + + A core principle is that each data object (DO) has a locale property behaving in the following way: + locales has 1 locale value, a locale object. + This is the most common use case. The property’s getter returns the user’s locale. + Fetching an object with a criteria asking for a specific locale will return an object in that locale. + Changing the locale property of an object to another locale instance (singleton in Locale’s case), updates all the values of its localizable properties to the new locale set. + locales has either no value, or “*” equivalent, an “All Locale Locale” + This feches the json structure and returns all the values in all the locales + locales has an array of locale instances. + If locale’s cardinality is > 1 then each localized property would return a json/dictionary of locale->value instead of 1 value. + */ + + readOperation.locales = self.userLocales; + + if (query.criteria) { + //readOperation.criteria = criteria.clone(); + readOperation.criteria = query.criteria; + } + if (query.sizeLimit) { + readOperation.data.sizeLimit = query.sizeLimit; + } + if (query.batchSize) { + readOperation.data.batchSize = query.batchSize; + } + if (query.orderings && query.orderings > 0) { + rawOrderings = []; + // self._mapObjectDescriptorOrderingsToRawOrderings(objectDescriptor, query.sortderings,rawOrderings); + // readOperation.data.orderings = rawOrderings; + readOperation.data.orderings = query.orderings; + } + if (query.readExpressions && query.readExpressions.length) { + readOperation.data.readExpressions = query.readExpressions; + } + /* + We need to do this in node's DataWorker, it's likely that we'll want that client side as well, where it's some sort of token set post authorization. + */ + if (self.application.identity && self.shouldAuthenticateReadOperation) { + readOperation.identity = self.application.identity; + } + + /* to save bandwidth, if it's true - the default, we won't include the key */ + if (!query.includesChildObjectDescriptors) { + readOperation.data.includesChildObjectDescriptors = + query.includesChildObjectDescriptors; + } + + if (query.hints) { + readOperation.hints = query.hints; + } + + /* + this is half-assed, we're mapping full objects to RawData, but not the properties in the expression. + phront-service does it, but we need to stop doing it half way there and the other half over there. + SaveChanges is cleaner, but the job is also easier there. + */ + let criteria = query.criteria, + parameters = criteria ? criteria.parameters : undefined; + rawParameters = parameters; + + // if (parameters && typeof criteria.parameters === "object") { + // var keys = Object.keys(parameters), + // i, + // countI, + // iKey, + // iValue, + // iRecord, + // criteriaClone; + + // //rawParameters = Array.isArray(parameters) ? [] : {}; + + // for (i = 0, countI = keys.length; i < countI; i++) { + // iKey = keys[i]; + // iValue = parameters[iKey]; + // if (!iValue) { + // console.warn( + // "fetchData: criteria ", + // criteria, + // "has value: " + value + " for parameter key " + iKey + // ); + // } else { + // if (iValue.dataIdentifier) { + // if (!criteriaClone) { + // criteriaClone = criteria.clone(); + // rawParameters = criteriaClone.parameters; + // } + // /* + // this isn't working because it's causing triggers to fetch properties we don't have + // and somehow fails, but it's wastefull. Going back to just put primary key there. + // */ + // // iRecord = {}; + // // rawParameters[iKey] = iRecord; + // // (promises || (promises = [])).push( + // // self._mapObjectToRawData(iValue, iRecord) + // // ); + // rawParameters[iKey] = iValue.dataIdentifier.primaryKey; + // } + // // else { + // // rawParameters[iKey] = iValue; + // // } + // } + // } + + // if (criteriaClone) { + // readOperation.criteria = criteriaClone; + // } + // } + + readOperation.dataStream = stream; + + self.registerReadEvent(readOperation); + + query.type.dispatchEvent(readOperation); + if (readOperation.propagationPromise) { + readOperation.propagationPromise.finally(() => { + self.unregisterReadEvent(readOperation); + }); + } else { + self.unregisterReadEvent(readOperation); + } + } catch (e) { + stream.dataError(e); + } + + // try { + // self._fetchDataWithOperation(query, stream); + // } catch (e) { + // stream.dataError(e); + // } + } else { + var service; + //This is a workaround, we should clean that up so we don't + //have to go up to answer that question. The difference between + //.TYPE and Objectdescriptor still creeps-in when it comes to + //the service to answer that to itself + if ( + self.parentService && + self.parentService.childServiceForType(query.type) === self && + typeof self.fetchRawData === "function" + ) { + service = self; + service._fetchRawData(stream); + } else { + // Use a child service to fetch the data. + try { + service = self.childServiceForType(query.type); + if (service) { + //Here we end up creating an extra stream for nothing because it should be third argument. + stream = service.fetchData(query, stream) || stream; + self._dataServiceByDataStream.set(stream, service); + } else { + throw new Error( + "Can't fetch data of unknown type - " + + (query.type.typeName || query.type.name) + + "/" + + query.type.uuid + ); + } + } catch (e) { + stream.dataError(e); + } + } + return service; + } + }) + ); + + //Ensure we clean up after all client processing the DataStream are done + //Don't like creating a dummy array though... + Promise.allSettled([stream]).then((results) => { + this.unregisterDataStream(stream); + }); + + // Return the passed in or created stream. + return stream; + }, + }, + + _fetchRawData: { + value: function (stream) { + var self = this, + childService = this._childServiceForQuery(stream.query); + + if (childService) { + childService._fetchRawData(stream); + } else { + //this is the new path for services with a authenticationPolicy + if (this.authenticationPolicy) { + var identityPromise, shouldAuthenticate; + //If this is the service providing providesIdentity for the query's type: IdentityService for the Identity type required. + //the query is either for the Identity itself, or something else. + //If it's for the user identity itself, it's a simple fetch + //but if it's for something else, we may need to fetch the identity first, and then move on to the query at hand. + if (this.providesIdentity) { + //Regardless of the policy, we're asked to fetch a user identity + var streamQuery = stream.query; + if (stream.query.criteria) { + stream.query = self.mapQueryToRawDataQuery(streamQuery); + } + this.fetchRawData(stream); + //Switch it back + stream.query = streamQuery; + } else { + if (stream.query.identity) { + identityPromise = Promise.resolve(stream.query.identity); + } else if (!this.identity) { + if (this.identityPromise) { + identityPromise = this.identityPromise; + } else if ( + this.authenticationPolicy === + AuthenticationPolicyType.UpfrontAuthenticationPolicy || + (this.authenticationPolicy === AuthenticationPolicy.ON_DEMAND && + (shouldAuthenticate = typeof this.queryRequireAuthentication === "function") && + this.queryRequireAuthentication(stream.query)) + ) { + this.identityPromise = identityPromise = new Promise(function (resolve, reject) { + var identityServices = this.identityServices, + identityObjectDescriptors, + selfUserCriteria, + identityQuery; + + //Shortcut, there could be multiple one we need to flatten. + identityObjectDescriptors = identityServices[0].types; + //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); + identityQuery = DataQuery.withTypeAndCriteria(identityObjectDescriptors[0]); + + this.rootService.fetchData(identityQuery).then( + function (userIdenties) { + self.identity = userIdenties[0]; + resolve(self.identity); + }, + function (error) { + console.error(error); + reject(error); + } + ); + }); + } else identityPromise = Promise.resolve(true); + } else { + identityPromise = Promise.resolve(true); + } + + identityPromise + .then(function (identity) { + var streamSelector = stream.query; + stream.query = self.mapQueryToRawDataQuery(streamSelector); + self.fetchRawData(stream); + stream.query = streamSelector; + }) + .catch(function (e) { + stream.dataError(e); + self.identityPromise = Promise.resolve(null); + }); + } + } else { + if (this.authorizationPolicy === AuthorizationPolicy.ON_DEMAND) { + var prefetchAuthorization = + typeof this.shouldAuthorizeForQuery === "function" && + this.shouldAuthorizeForQuery(stream.query); + if (prefetchAuthorization || !this.authorization) { + this.authorizationPromise = exports.DataService.authorizationManager + .authorizeService(this, prefetchAuthorization) + .then(function (authorization) { + self.authorization = authorization; + return authorization; + }); + } + } + + this.authorizationPromise + .then(function (authorization) { + var streamSelector = stream.query; + stream.query = self.mapQueryToRawDataQuery(streamSelector); + self.fetchRawData(stream); + stream.query = streamSelector; + }) + .catch(function (e) { + stream.dataError(e); + self.authorizationPromise = Promise.resolve(null); + }); + } + } + }, + }, + + /* + ObjectDescriptors are what should dispatch events, as well as model objec intances + So an imperative method on DataService would internally create the operation/event and dispatch it on the object descriptor. It would internally addEventListener for the "symetric event", for a Read, it would be a ReadUpdated, for a ReadUpdate/ a ReadUpdated as well. + + This would help re-implementing fetchData to be backward compatible. + + What is clear is that we need for a read for example, a stable array that can be relied on by bindings and be mutated over time. + + If we have anObjecDescriptor.dispatchEvent("read"), then someone whose job it to act on that read is an EventListener for it, and that's the RawDataServices running in the DataServiceWorker. This is where we need an intermediary whose job is to send these events over postMesssage so they get dispatched in the DataServiceWorker. + + So as a developer what am I writing if I'm not doing + + mainService.fetchData(query); //? + + mainService.readData(query); //? + mainService.saveData(query); //? + + mainService.performOperation(readOperation); + + + */ + + readData: { + value: function (dataOperation, optionalStream) { + var self = this, + objectDescriptor = dataOperation.objectDescriptor || dataOperation.dataType, + stream = optionalStream, + dataService, + dataServicePromise; + + // Set up the stream. + stream = stream || new DataStream(); + stream.operation = dataOperation; + + if (!(dataService = this._dataServiceByDataStream.get(stream))) { + this._dataServiceByDataStream.set( + stream, + (dataServicePromise = this._childServiceRegistrationPromise.then(function () { + var service; + //This is a workaround, we should clean that up so we don't + //have to go up to answer that question. The difference between + //.TYPE and Objectdescriptor still creeps-in when it comes to + //the service to answer that to itself + if ( + self.parentService && + self.parentService.childServiceForType(objectDescriptor) === self && + typeof self.fetchRawData === "function" + ) { + service = self; + service._fetchRawData(stream); + } else { + // Use a child service to fetch the data. + service = self.childServiceForType(objectDescriptor); + if (service) { + //Here we end up creating an extra stream for nothing because it should be third argument. + self._dataServiceByDataStream.set(stream, service); + } else { + throw new Error("Can't fetch data of unknown type - " + objectDescriptor.name); + } + } + + return service; + })) + ); + } else { + dataServicePromise = Promise.resolve(dataService); + } + + dataServicePromise.then(function (dataService) { + try { + //Direct access for now + stream = dataService.handleRead(dataOperation); + } catch (e) { + stream.dataError(e); + } + }); + + // Return the passed in or created stream. + return stream; + }, + }, + + _childServiceForQuery: { + value: function (query) { + var serviceModuleID = this._serviceIdentifierForQuery(query), + service = serviceModuleID && this._childServicesByIdentifier.get(serviceModuleID); + + if (!service && this._childServicesByType.has(query.type)) { + service = this._childServicesByType.get(query.type); + service = service && service[0]; + } + + return service || null; + }, + }, + + _serviceIdentifierForQuery: { + value: function (query) { + var parameters = query && query.criteria ? query.criteria.parameters : null, + serviceModuleID = parameters && parameters.serviceIdentifier, + mapping, + propertyName; + + if (!serviceModuleID) { + mapping = this.mappingForType(query.type); + propertyName = mapping && parameters && parameters.propertyName; + serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName); + } + + return serviceModuleID; + }, + }, + + __dataServiceByDataStream: { + value: null, + }, + + _dataServiceByDataStream: { + get: function () { + return this.__dataServiceByDataStream || (this.__dataServiceByDataStream = new WeakMap()); + }, + }, + + dataServiceForDataStream: { + get: function (dataStream) { + return this._dataServiceByDataStream.get(dataStream); + }, + }, + + /** + * To be called to indicates that the consumer has lost interest in the passed DataStream. + * This will allow the RawDataService feeding the stream to take appropriate measures. + * + * @method + * @argument {DataStream} [dataStream] - The DataStream to cancel + * @argument {Object} [reason] - An object indicating the reason to cancel. + * + */ + cancelDataStream: { + value: function (dataStream, reason) { + if (dataStream) { + var rawDataService = this._dataServiceByDataStream.get(dataStream), + self = this; + + if (Promise.is(rawDataService)) { + rawDataService.then(function (service) { + self._cancelServiceDataStream(service, dataStream, reason); + }); + } else { + this._cancelServiceDataStream(rawDataService, dataStream, reason); + } + } + }, + }, + + _cancelServiceDataStream: { + value: function (rawDataService, dataStream, reason) { + rawDataService.cancelRawDataStream(dataStream, reason); + this._dataServiceByDataStream.delete(dataStream); + }, + }, + + /** + * update data - potentially multiple instances - using the service using its child services, + * without requiring to fetch them as much as possible, depending on the capabilities of DataServices involved. + + * the changes will be regsitered in the data service and applied when mainDataService.saveChanges() is called. + * + * Later on support should be added for the ability to have multiple transactions being worked on in parallel. While it would be possible here + * de pass a transaction instance as an argument, we can't really do that when automatically tracking changes on + * instances. To do so, we can't beat the notion of an "EditingContex". I would make sense that EditingContext + * become the side of DataService exposed to the app side. + * + * + * @method + * @argument {ObjectDescriptor|Function|String} + * objectDescriptorOrType - defines what type of data + * should be updated, and the criteria + * that data should satisfy can be defined + * using the `criteria` argument. A type + * is defined as either a DataObjectDesc- + * riptor, an Object Descriptor, a Construct- + * or the string module id. The method will + * convert the passed in type to a Data- + * ObjectDescriptor (deprecated) or an + * ObjectDescriptor. This is true whether + * passing in a DataQuery or a type. + * @argument {?Criteria} + * optionalCriteria - If provided, defines the criteria which + * data to update should satisfy. + * @argument {Object} + * values - The values to apply to data described by optionalCriteria. Follows the shape used internally by DataService to track changes. + * "toOneProperty": value, + * "toManyProperty": { + * addedValues: [], + * removedValues: [] + * } + * + * @returns undefined + * + */ + updateData: { + value: function (objectDescriptorOrTypeOrString, criteria, values) { + this.registerDataUpdate( + objectDescriptorOrTypeOrString instanceof ObjectDescriptor + ? objectDescriptorOrTypeOrString + : this.objectDescriptorForType(objectDescriptorOrTypeOrString) + ); + }, + }, + dataUpdates: { + get: function () { + if (this.isRootService) { + if (!this._dataUpdates) { + this._dataUpdates = new Map(); + } + return this._dataUpdates; + } else { + return this.rootService.dataUpdates; + } + }, + }, + + registerDataUpdate: { + value: function (objectDescriptor, criteria, changes) { + var dataUpdates = this.dataUpdates, + value = dataUpdates.get(objectDescriptor); + if (!value) { + dataUpdates.set(objectDescriptor, (value = new Map())); + } + value.set(criteria, changes); + this.objectDescriptorsWithChanges.add(objectDescriptor); + }, + }, + + unregisterDataUpdate: { + value: function (objectDescriptor, criteria, changes) { + var value = this.dataUpdates.get(objectDescriptor); + if (value) { + value.delete(criteria); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + }, + }, + + /** + * delete data - potentially multiple instances - using the service using and it's child services, + * without requiring to fetch them as much as possible, depending on the capabilities of DataServices involved. + * + * the changes will be regsitered in the data service and applied when mainDataService.saveChanges() is called. + * + * Later on support should be added for the ability to have multiple transactions. While it would be possible here + * de pass a transaction instance as an argument, we can't really do that when automatically tracking changes on + * instances. To do so, we can't beat the notion of an "EditingContex". I would make sense that EditingContext + * become the side of DataService exposed to the app side. + * + * The requested data may be fetched asynchronously, in which case the data + * stream will be returned immediately but the stream's data will be added + * to the stream at a later time. + * + * @method + * @argument {ObjectDescriptor|Function|String} + * objectDescriptorOrType - defines what type of data + * should be updated, and the criteria + * that data should satisfy can be defined + * using the `criteria` argument. A type + * is defined as either a DataObjectDesc- + * riptor, an Object Descriptor, a Construct- + * or the string module id. The method will + * convert the passed in type to a Data- + * ObjectDescriptor (deprecated) or an + * ObjectDescriptor. This is true whether + * passing in a DataQuery or a type. + * @argument {?Criteria} + * optionalCriteria - If provided, defines the criteria which + * data to delete should satisfy. + * + * @returns undefined + * + */ + deleteData: { + value: function (objectDescriptorOrTypeOrString, criteria) { + this.registerDataDelete( + objectDescriptorOrTypeOrString instanceof ObjectDescriptor + ? objectDescriptorOrTypeOrString + : this.objectDescriptorForType(objectDescriptorOrTypeOrString) + ); + }, + }, + dataDeletes: { + get: function () { + if (this.isRootService) { + if (!this._dataDeletes) { + this._dataDeletes = new Map(); + } + return this._dataDeletes; + } else { + return this.rootService.dataDeletes; + } + }, + }, + + registerDataDelete: { + value: function (objectDescriptor, criteria) { + var dataDeletes = this.dataDeletes, + value = dataDeletes.get(objectDescriptor); + if (!value) { + dataDeletes.set(objectDescriptor, (value = new Set())); + } else { + if (value.has(null)) { + if (criteria) { + console.warn( + "All instances of " + + objectDescriptor.name + + " have been set to be deleted, but the current call uses a criteria: ", + criteria + ); + } + } else if (!criteria) { + console.warn( + "Some instances of " + + objectDescriptor.name + + " have been set to be deleted if matching criteria: ", + criteria, + " but the current delete will delete all instances" + ); + } + } + + value.add(criteria || null); + + this.objectDescriptorsWithChanges.add(objectDescriptor); + }, + }, + + unregisterDataDelete: { + value: function (objectDescriptor, criteria) { + var value = this.dataDeletes.get(objectDescriptor); + if (value) { + value.delete(criteria); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + }, + }, + + /** + * EventChange handler, begining of tracking objects changes via Triggers right now, + * which are installed on propertyDescriptors. We might need to refine that by adding the + * ability to model wether a property is persisted or not. If it's not meant to be persisted, + * then a DataService most likely doesn't have much to do with it. + * Right now, this is unfortunately called even during the mapRawDataToObject. + * We need a way to ignore this as early as possible + * + * @method + * @argument {ChangeEvent} [changeEvent] - The changeEvent + * + */ + handleChange: { + value: function (changeEvent) { + //Adding check to avoid changes from property-fetching + let trigger = this._triggerForObjectProperty(changeEvent.target, changeEvent.key), + triggerValueStatus = trigger._getValueStatus(changeEvent.target); + + /* + If triggerValueStatus is a promise, then we know the property is being fetched. + BUT HOW CAN WE BE SURE THAT THIS CHANGE IS THE RESULT OF THE FETCH? + + But that that specific change Event could still be a change coming from user-side? + + - The snapshot may not help us since it could be a property resolved off the primary key + if(trigger._getValueStatus(object) == ) + + */ + //Commentting out the restriction to exclude created objects as we might want to + //use it for them as well + + // if(!this._createdDataObjects || (this._createdDataObjects && !this._createdDataObjects.has(changeEvent.target))) { + //Needs to register the change so saving changes / update operations can use it later to decise what to send + //console.log("handleChange:",changeEvent); + this.registerDataObjectChangesFromEvent(changeEvent); + //} + }, + }, + + /*************************************************************************** + * Saving Data + */ + + /** + * Delete a data object. + * + * @method + * @argument {Object} object - The object whose data should be deleted. + * @returns {external:Promise} - A promise fulfilled when the object has + * been deleted. + */ + deleteDataObject: { + value: function (object) { + var saved = !this.isObjectCreated(object); + this.registerDeletedDataObject(object); + return this._updateDataObject(object, saved && "deleteDataObject"); + }, + }, + + /** + * Resets an object to the last value in the snapshot. + * @method + * @argument {Object} object - The object who will be reset. + * @returns {external:Promise} - A promise fulfilled when the object has + * been mapped back to its last known state. + */ + resetDataObject: { + value: function (object) { + var service = this._getChildServiceForObject(object), + promise; + + if (service) { + promise = service.resetDataObject(object); + } + + return promise; + }, + }, + + /** + * Save changes made to a data object. + * + * @method + * @argument {Object} object - The object whose data should be saved. + * @returns {external:Promise} - A promise fulfilled when all of the data in + * the changed object has been saved. + */ + saveDataObject: { + value: function (object) { + //return this._updateDataObject(object, "saveDataObject"); + + //TODO + //First thing we should be doing here is run validation + //on the object, using objectDescriptor's + //draft: - could/should become async and return a promise. + //var validatity = this.objectDescriptorForObject(object).evaluateRules(object); + + var self = this, + service, + promise = this.nullPromise, + mappingPromise; + + if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { + var record = {}; + mappingPromise = this._mapObjectToRawData(object, record); + if (!mappingPromise) { + mappingPromise = this.nullPromise; + } + return mappingPromise.then(function () { + return self.saveRawData(record, object).then(function (data) { + self.rootService.createdDataObjects.delete(object); + return data; + }); + }); + } else { + service = this._getChildServiceForObject(object); + if (service) { + var result = service.saveDataObject(object); + if (result) { + return result.then( + function (success) { + self.rootService.createdDataObjects.delete(object); + //Duck test of an operation + // if(success.data) { + // return success.data; + // } + // else { + return success; + // } + }, + function (error) { + console.error(error); + return Promise.reject(error); + } + ); + } + } else { + return promise; + } + } + }, + }, + + _updateDataObject: { + value: function (object, action) { + var self = this, + service, + promise = this.nullPromise; + + if (this.parentService && this.parentService._getChildServiceForObject(object) === this) { + service = action && this; + } else { + service = action && this._getChildServiceForObject(object); + if (service) { + return service._updateDataObject(object, action); + } + } + + if (!action) { + self.unregisterCreatedDataObject(object); + } else if (service) { + promise = service[action](object).then(function () { + self.unregisterCreatedDataObject(object); + return null; + }); + } + return promise; + }, + }, + + _saveDataObject: { + value: function (object) { + var record = {}; + this._mapObjectToRawData(object, record); + return this.saveRawData(record, object); + }, + }, + // _updateDataObject: { + // value: function (object, action) { + // var self = this, + // service = action && this._getChildServiceForObject(object), + // promise = this.nullPromise; + + // if (!action) { + // self.createdDataObjects.delete(object); + // } else if (service) { + // promise = service[action](object).then(function () { + // self.createdDataObjects.delete(object); + // return null; + // }); + // } + // return promise; + // } + // }, + + /** + * Returns a pomise of a DataOperation to save (create/updates) pending changes to an object + * @method + * @argument {Object} object - The object who will be reset. + * @returns {external:Promise} - A promise fulfilled when the data operation is ready. + */ + saveDataOperationFoObject: { + value: function (object) {}, + }, + + /** + * Save all changes made since the last call. This method currently delegates to rawDataServices for the actual work + * Some of it might migrate back up here later when the dust settles. + * + * TEMPORARY implementation assuming a single RawDataService that creates + * operations. We might need to officially make that kind of subclass the + * mainService. + * + * @method + * @returns {external:Promise} - A promise fulfilled when the save operation is complete or failed. + */ + + // saveChanges: { + // value: function () { + // var self = this, + // promise = this.nullPromise, + // service = this.childServices[0]; + + // if (service && typeof service.saveChanges === "function") { + // return service.saveChanges(); + // } + // else { + // return promise; + // } + // } + // }, + + /*************************************************************************** + * Offline + */ + + _initializeOffline: { + value: function () { + // TODO: This code assumes that the first instance of DataService or + // of one of its subclasses is either the + // root service, and that no instance of DataService subclasses are. + // This needs to be fixed to allow DataService child services and + // DataService subclass root services. + var self = this; + if ( + typeof global.addEventListener === "function" && + !exports.DataService.prototype._isOfflineInitialized + ) { + exports.DataService.prototype._isOfflineInitialized = true; + global.addEventListener("online", function (event) { + self.rootService.isOffline = false; + }); + global.addEventListener("offline", function (event) { + self.rootService.isOffline = true; + }); + } + }, + }, + + _isOfflineInitialized: { + value: false, + }, + + /** + * Returns a value derived from and continuously updated with the value of + * [navigator.onLine]{@link https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine}. + * + * Root services are responsible for tracking offline status, and subclasses + * not designed to be root services should override this property to get + * its value from their root service. + * + * @type {boolean} + */ + isOffline: { + get: function () { + if (this._isOffline === undefined) { + // Determine the initial value from the navigator state and call + // the public setter so _goOnline() is invoked if appropriate. + this.isOffline = !navigator.onLine; + } + return this._isOffline; + }, + set: function (offline) { + var self = this; + if (this._willBeOffline === null) { + // _goOnline() just finished, set _isOffline to the desired + // value and clear the "just finished" flag in _willBeOffline. + this._isOffline = offline ? true : false; + this._willBeOffline = undefined; + } else if (this._willBeOffline !== undefined) { + // _goOnline() is in progress, just record the future value. + this._willBeOffline = offline ? true : false; + } else if (this._isOffline === false) { + // Already online and not starting up, no need for _goOnline(). + this._isOffline = offline ? true : false; + } else if (!offline) { + // Going from offline to online, or starting up online, so + // assume we were last offline, call _goOnline(), and only + // change the value when that's done. + this._isOffline = true; + this._willBeOffline = false; + this._goOnline().then(function () { + var offline = self._willBeOffline; + self._willBeOffline = null; + self.isOffline = offline; + return null; + }); + } + }, + }, + + _isOffline: { + // `undefined` on startup, otherwise always `true` or `false`. + value: false, + }, + + _willBeOffline: { + // `true` or `false` while _goOnline() is in progress, `null` just after + // it's done, `undefined` otherwise. + value: undefined, + }, + + _goOnline: { + value: function () { + var self = this; + return this.readOfflineOperations() + .then(function (operations) { + operations.sort(this._compareOfflineOperations); + return self.performOfflineOperations(operations); + }) + .catch(function (e) { + console.error(e); + }); + }, + }, + + _compareOfflineOperations: { + value: function (operation1, operation2) { + // TODO: Remove reference to `lastModified` once child services have + // been udpated to use `time` instead. + return operation1.lastModified < operation2.lastModified + ? -1 + : operation1.lastModified > operation2.lastModified + ? 1 + : operation1.time < operation2.time + ? -1 + : operation1.time > operation2.time + ? 1 + : operation1.index < operation2.index + ? -1 + : operation1.index > operation2.index + ? 1 + : 0; + }, + }, + + /** + * Reads all the offline operations recorded on behalf of this service. + * + * The default implementation aggregates this service children's offline + * operations, keeping track of which child service is responsible for each + * operation. + * + * Subclasses that provide offline support should override this method to + * return the operations that have been performed while offline. + * + * @method + */ + readOfflineOperations: { + value: function () { + // TODO: Get rid of the dummy WeakMap passed to children once the + // children's readOfflineOperations code has been updated to not + // expect it. + // This implementation avoids creating promises for services with no + // children or whose children don't have offline operations. + var self = this, + dummy = new WeakMap(), + services = this._offlineOperationServices, + array, + promises; + this.childServices.forEach(function (child) { + var promise = child.readOfflineOperations(dummy); + if (promise !== self.emptyArrayPromise) { + array = array || []; + promises = promises || []; + promises.push( + promise.then(function (operations) { + var i, n; + for (i = 0, n = operations && operations.length; i < n; i += 1) { + services.set(operations[i], child); + array.push(operations[i]); + } + return null; + }) + ); + } + }); + return promises + ? Promise.all(promises).then(function () { + return array; + }) + : this.emptyArrayPromise; + }, + }, + + /** + * @private + * @type {Map} + */ + _offlineOperationServices: { + get: function () { + if (!this.__offlineOperationServices) { + this.__offlineOperationServices = new WeakMap(); + } + return this.__offlineOperationServices; + }, + }, + + __offlineOperationServices: { + value: undefined, + }, + + /** + * Perform operations recorded while offline. This will be invoked when the + * service comes online after being offline. + * + * The default implementation delegates performance of each operation to + * the child service responsible for that operation, as determined by + * [readOfflineOperations()]{@link DataService#readOfflineOperations}. It + * will batch operations if several consecutive operations belong to the + * same child service. + * + * For each operation not handled by a child service, the default + * implementation calls a method named `performFooOfflineOperation()`, if + * such a method exists in this service where `foo` is the operation's + * [data type]{@link DataOperation#dataType}. If no such method exists, + * [readOfflineOperation()]{@link DataService#readOfflineOperation} is + * called instead. + * + * Subclasses that provide offline support should implement these + * `performFooOfflineOperation()` methods or override the + * `readOfflineOperation()` method to perform each operation, or they can + * override this `performOfflineOperations()` method instead. + * + * Subclass overriding this method are responsible for + * [deleting]{@link DataService#deleteOfflineOperations} operations after + * they have been performed. Subclasses implementing + * `performFooOfflineOperation()` methods or overriding the + * `readOfflineOperation()` method are not. + * + * @method + * @argument {Array.} - operations + * @returns {Promise} - A promise fulfilled with a null value when the + * operations have been performed, or rejected if a problem occured that + * should prevent following operations from being performed. + */ + performOfflineOperations: { + value: function (operations) { + var services = this._offlineOperationServices, + promise = this.nullPromise, + child, + i, + j, + n, + jOperation, + jOperationChanges, + jService, + jOperationType, + jTableSchema, + jForeignKeys, + OfflineService = OfflineService, + k, + countK, + kForeignKey, + kOnlinePrimaryKey; + + // Perform each operation, batching if possible, and collecting the + // results in a chain of promises. + for (i = 0, n = operations.length; i < n; i = j) { + // Find the service responsible for this operation. + child = services.get(operations[i]); + // Find the end of a batch of operations for this service. + j = i + 1; + while (j < n && child && (jService = services.get((jOperation = operations[j]))) === child) { + ++j; + } + // Add the promise to perform this batch of operations to the + // end of the chain of promises to fulfill all operations. + promise = this._performOfflineOperationsBatch(promise, child, operations, i, j); + } + // Return a promise for the sequential fulfillment of all operations. + return promise; + }, + }, + + _performOfflineOperationsBatch: { + value: function (promise, child, operations, start, end) { + var self = this; + return promise.then(function () { + return child + ? child.performOfflineOperations(operations.slice(start, end)) + : self._performAndDeleteOfflineOperation(operations[start]); + }); + }, + }, + + _performAndDeleteOfflineOperation: { + value: function (operation) { + //Before we perform an operation, we need to look a foreignKeys in jOperation changes to update if needed before performing the operation + //if we don't have a known list of foreign keys, we'll consider all potential candidate + var self = this, + operationType = operation.type, + tableSchema, + foreignKeys, + k, + countK, + kOnlinePrimaryKey, + kForeignKey; + + if (this.offlineService) { + tableSchema = this.offlineService.schema[operationType]; + foreignKeys = tableSchema.foreignKeys; + } + + if (!foreignKeys) { + foreignKeys = + tableSchema._computedForeignKeys || + (tableSchema._computedForeignKeys = Object.keys(operation.changes)); + } + + for (k = 0, countK = foreignKeys.length; k < countK; k++) { + kForeignKey = foreignKeys[k]; + //If a previous operation resulted in an online primaryKey replacing an offline one, + //we update the operation's changes accordingly + if ( + (kOnlinePrimaryKey = this.onlinePrimaryKeyForOfflinePrimaryKey(operation.changes[kForeignKey])) + ) { + operation.changes[kForeignKey] = kOnlinePrimaryKey; + } + } + + return this._performTypedOfflineOperation(operation).then(function () { + return self.deleteOfflineOperations([operation]); + }); + }, + }, + + _performTypedOfflineOperation: { + value: function (operation) { + // TODO: Remove support for operation.type once all child services + // have been updated to provide an operation.dataType instead. + var type = operation.dataType || operation.type, + method = type && this[this._getOfflineOperationMethodName(type)]; + return typeof method === "function" + ? method.call(this, operation) + : this.performOfflineOperation(operation); + }, + }, + + _getOfflineOperationMethodName: { + value: function (type) { + var isString = typeof type === "string", + name = isString && this._offlineOperationMethodNames.get(type); + if (isString && !name) { + name = "perform"; + name += type[0].toUpperCase(); + name += type.slice(1); + name += "OfflineOperation"; + this._offlineOperationMethodNames.set(type, name); + } + return name; + }, + }, + + _offlineOperationMethodNames: { + value: new Map(), + }, + + /** + * Called from + * [performOfflineOperations()]{@link DataService#performOfflineOperations} + * to perform a particular operation when no more specific + * `performFooOfflineOperation()` method is available for that operation, + * where `Foo` is the operation's [data type]{@link DataOperation#dataType}. + * + * The default implementation does nothing. + * + * Subclass overriding this method do not need to + * [delete]{@link DataService#deleteOfflineOperations} the passed in + * operation after it has successfully been performed: The method calling + * this method will take care of that. + * + * @method + * @argument {DataOperation} operation + * @returns {Promise} - A promise fulfilled with a null value when the + * operation has been performed, or rejected if a problem occured that + * should prevent following operations from being performed. + */ + performOfflineOperation: { + value: function (operation) { + // To be overridden by subclasses that use offline operations. + return this.nullPromise; + }, + }, + + // To be overridden by subclasses as necessary + onlinePrimaryKeyForOfflinePrimaryKey: { + value: function (offlinePrimaryKey) { + return this.offlineService + ? this.offlineService.onlinePrimaryKeyForOfflinePrimaryKey(offlinePrimaryKey) + : null; + }, + }, + + /** + * Delete operations recorded while offline. + * + * Services overriding the (plural) + * [performOfflineOperations()]{@link DataService#performOfflineOperations} + * method must invoke this method after each operation they perform is + * successfully performed. + * + * This method will be called automatically for services that perform + * operations by implementing a + * [performOfflineOperation()]{@link DataService#performOfflineOperation} + * or `performFooOfflineOperation()` methods (where `foo` is an operation + * [data type]{@link DataOperation#dataType}). + * + * Subclasses that provide offline operations support must override this + * method to delete the specified offline operations from their records. + * + * @method + * @argument {Array.} operations + * @returns {Promise} - A promise fulfilled with a null value when the + * operations have been deleted. + */ + deleteOfflineOperations: { + value: function (operations) { + // To be overridden by subclasses that use offline operations. + return this.nullPromise; + }, + }, + + /*************************************************************************** + * + * Access Control related methods + * + ***************************************************************************/ + + _performsAccessControl: { + value: false, + }, + performsAccessControl: { + get: function () { + return this._performsAccessControl; + }, + set: function (value) { + if (this._performsAccessControl !== value) { + /* + We're now doing work unrelated to acces control in captureReadOperation, + so we don't need to do this here anymore + */ + // if(this._performsAccessControl && !value) { + // this.application.removeEventListener(DataOperation.Type.ReadOperation,this,true); + // } + this._performsAccessControl = value; + + //Until we have a generic prepareForActivation + //Intended for access control: we capture at the highest level: + if (this.performsAccessControl) { + /* + We're now doing work unrelated to acces control in captureReadOperation, + so we don't need to do this here anymore + */ + //this.application.addEventListener(DataOperation.Type.ReadOperation,this,true); + /* + To assess ObjectDescriptor-level access + */ + this.application.addEventListener(DataOperation.Type.CreateTransactionOperation, this, true); + + /* + To assess AppendTransactionOperation's operations access + */ + this.application.addEventListener(DataOperation.Type.AppendTransactionOperation, this, true); + + /* + To assess PerformTransactionOperation's operations access + */ + this.application.addEventListener(DataOperation.Type.PerformTransactionOperation, this, true); + } + } + }, + }, + + _accessPolicies: { + value: undefined, + }, + accessPolicies: { + get: function () { + if (!this._accessPolicies) { + this._accessPolicies = []; + this._accessPolicies.addRangeChangeListener(this, "accessPolicies"); + } + return this._accessPolicies; + }, + }, + handleAccessPoliciesRangeChange: { + value: function (plus, minus, index) { + //if (!this.isDeserializing) { + this._registerAccessPoliciesByObjectDescriptor(plus); + this._unregisterAccessPoliciesByObjectDescriptor(minus); + //} + + // var i, countI, iAccessPolicy, iObjectDescriptor, + // j, countJ, jOperationTypes; + // for(i=0, countI = plus.length; (i !arrB.includes(x)); + + or + + - look at the intersection between the object descriptors in the transaction and a list of non-authorized ones. That intersection should be empty. + + */ + captureCreateTransactionOperation: { + value: function (createTransactionOperation) { + //console.log("captureCreateTransactionOperation: "+createTransactionOperation.target.name+", criteria.expression: "+createTransactionOperation.criteria.expression+", criteria.parameters: "+JSON.stringify(createTransactionOperation.criteria.parameters), createTransactionOperation); + + //console.log("objectDescriptors: ", createTransactionOperation.objectDescriptors); + + if (this.performsAccessControl) { + var self = this, + result = this.evaluateAccessPoliciesForDataOperation(createTransactionOperation); + + if (this._isAsync(result)) { + /* + Returning a promise from the event handler ensures the next listener inline doesn't get involed until we're done. + */ + return result.then(function (value) { + return self._captureCreateTransactionOperationPostAccessPoliciesEvaluation( + createTransactionOperation + ); + }); + } else { + this._captureCreateTransactionOperationPostAccessPoliciesEvaluation(createTransactionOperation); + } + } + }, + }, + + /** + * If the operation isn't authorized, stopImmediatePropagation and dispatch the appropriate operation/event to say so. + * Returns the fail operation if created. + * + * @method + * @argument {DataOperation} appendTransactionOperation + * @returns undefined + */ + _dispatchTransactionFailedOperationFor: { + value: function (transactionOperation, error) { + transactionOperation.stopImmediatePropagation(); + + var appendTransactionFailedOperation = new DataOperation(); + + appendTransactionFailedOperation.referrerId = transactionOperation.id; + //Make it work for both AppendTransactionOperation and PerformTransactionOperation + appendTransactionFailedOperation.type = + transactionOperation.type === DataOperation.Type.AppendTransactionOperation + ? DataOperation.Type.AppendTransactionFailedOperation + : DataOperation.Type.PerformTransactionFailedOperation; + appendTransactionFailedOperation.target = transactionOperation.target; + appendTransactionFailedOperation.context = transactionOperation.context; + appendTransactionFailedOperation.clientId = transactionOperation.clientId; + appendTransactionFailedOperation.data = error ? error : new Error("Unauthorized"); + + console.log( + "Unauthorized " + transactionOperation.type + " for type: " + transactionOperation.target.name, + transactionOperation + ); + + appendTransactionFailedOperation.target.dispatchEvent(appendTransactionFailedOperation); + return appendTransactionFailedOperation; + }, + }, + + __evaluateCaptureTransactionOperationAuthorized: { + value: function (iOperation, failedOperations) { + var result; + + result = this.evaluateAccessPoliciesForDataOperation(iOperation); + if (this._isAsync(result)) { + /* + Returning a promise from the event handler ensures the next listener inline doesn't get involed until we're done. + */ + var self = this; + return result.then(function (value) { + if (!self.isDataOperationAuthorized(iOperation)) { + failedOperations.push(iOperation); + return false; + } else { + return true; + } + }); + } else { + if (!this.isDataOperationAuthorized(iOperation)) { + failedOperations.push(iOperation); + return false; + } else { + return true; + } + } + }, + }, + + /** + * Returns the first Fail Operation if evaluation is synchronous so we can stop as early as possible + * + * @type {Set} + */ + _evaluateCaptureTransactionOperationAuthorized: { + value: function ( + transactionOperation, + operations, + failedOperations, + operationAccessPoliciesEvaluationPromises, + abortAtFirstNonAuthorized + ) { + var self = this, + i, + countI, + iOperation, + iResult, + iFailOperation, + iPromise; + + for (i = 0, countI = operations.length; i < countI; i++) { + iOperation = operations[i]; + + /* + So far, we've been setting the clientId as the WebSocket connectionId given by AWS API Gateway, from the lambda, which saves having to send it back en forth which HTTP connection would have to do. + + But we're setting that on the appendTransaction operation, which contains others, so to avoid looping on all to do it, we do it as we go. + */ + if (!iOperation.clientId) { + iOperation.clientId = transactionOperation.clientId; + } + if (!iOperation.identity) { + iOperation.identity = transactionOperation.identity; + } + if (!iOperation.context) { + iOperation.context = transactionOperation.context; + } + + iResult = this.__evaluateCaptureTransactionOperationAuthorized(iOperation, failedOperations); + + if (this._isAsync(iResult)) { + ( + operationAccessPoliciesEvaluationPromises || + (operationAccessPoliciesEvaluationPromises = []) + ).push( + (iPromise = iResult.then(function (isAuthorized) { + if (!isAuthorized && abortAtFirstNonAuthorized) { + // iFailOperation = self._dispatchTransactionFailedOperationFor(transactionOperation); + throw new Error("Not Authorized"); + } + })) + ); + return iPromise; + } else if (!iResult /*isAuthorized*/ && abortAtFirstNonAuthorized) { + //iFailOperation = self._dispatchTransactionFailedOperationFor(transactionOperation); + return iResult; + } + } + + return true; + }, + }, + + captureAppendTransactionOperation: { + value: function (appendTransactionOperation) { + return this.performsAccessControlForTransactionOperation(appendTransactionOperation); + }, + }, + + performsAccessControlForTransactionOperation: { + value: function (transactionOperation) { + if (this.performsAccessControl) { + /* + Returning a promise from the event handler ensures the next listener inline doesn't get involed until we're done. + */ + var self = this; + return new Promise(function (resolve, reject) { + try { + var operations = transactionOperation.data.operations, + objectDescriptorModuleIds = Object.keys(operations), + i, + countI, + iOperation, + iOperationsByType, + iResult, + isAuthorized, + operationAccessPoliciesEvaluationPromises = [], + operationAccessPoliciesEvaluationPromise, + push = Array.prototype.push, + /* + For debug purpose this could be turned to false + */ + abortAtFirstNonAuthorized = true, + failedOperations = []; + + /* + + operations is an object: { + "moduleId": { + "createOperations": [], + "updateOperations": [], + "deleteOperations": [] + } + } + */ + + for (i = 0, countI = objectDescriptorModuleIds.length; i < countI; i++) { + iObjectDescriptorModuleId = objectDescriptorModuleIds[i]; + iOperationsByType = operations[iObjectDescriptorModuleId]; + + if (iOperationsByType.createOperations) { + isAuthorized = self._evaluateCaptureTransactionOperationAuthorized( + transactionOperation, + iOperationsByType.createOperations, + failedOperations, + operationAccessPoliciesEvaluationPromises, + abortAtFirstNonAuthorized + ); + if ( + typeof isAuthorized === "boolean" && + !isAuthorized && + abortAtFirstNonAuthorized + ) { + break; + } + } + if (iOperationsByType.updateOperations) { + isAuthorized = self._evaluateCaptureTransactionOperationAuthorized( + transactionOperation, + iOperationsByType.updateOperations, + failedOperations, + operationAccessPoliciesEvaluationPromises, + abortAtFirstNonAuthorized + ); + if ( + typeof isAuthorized === "boolean" && + !isAuthorized && + abortAtFirstNonAuthorized + ) { + break; + } + } + if (iOperationsByType.mergeOperations) { + isAuthorized = self._evaluateCaptureTransactionOperationAuthorized( + transactionOperation, + iOperationsByType.mergeOperations, + failedOperations, + operationAccessPoliciesEvaluationPromises, + abortAtFirstNonAuthorized + ); + if ( + typeof isAuthorized === "boolean" && + !isAuthorized && + abortAtFirstNonAuthorized + ) { + break; + } + } + if (iOperationsByType.deleteOperations) { + isAuthorized = self._evaluateCaptureTransactionOperationAuthorized( + transactionOperation, + iOperationsByType.deleteOperations, + failedOperations, + operationAccessPoliciesEvaluationPromises, + abortAtFirstNonAuthorized + ); + if ( + typeof isAuthorized === "boolean" && + !isAuthorized && + abortAtFirstNonAuthorized + ) { + break; + } + } + } + + // for(i = 0, countI = operations.length; (i 0) { + if (!self.delegate) { + iFailOperation = + self._dispatchTransactionFailedOperationFor( + transactionOperation + ); + reject(iFailOperation.data); + } else { + if ( + self.callDelegateMethod( + "dataServiceAccessControlFailedForOperations", + self, + failedOperations + ) + ) { + iFailOperation = + self._dispatchTransactionFailedOperationFor( + transactionOperation + ); + reject(iFailOperation.data); + } else { + resolve(true); + } + } + } else { + resolve(true); + } + }) + .catch(function (error) { + if (!self.delegate) { + iFailOperation = self._dispatchTransactionFailedOperationFor( + transactionOperation, + error + ); + reject(iFailOperation.data); + } else { + if ( + self.callDelegateMethod( + "dataServiceAccessControlFailedForOperations", + self, + failedOperations + ) + ) { + iFailOperation = self._dispatchTransactionFailedOperationFor( + transactionOperation, + error + ); + reject(iFailOperation.data); + } else { + resolve(true); + } + } + }); + } else { + if (!self.delegate) { + iFailOperation = self._dispatchTransactionFailedOperationFor( + transactionOperation, + new Error("Unauthorized") + ); + reject(iFailOperation.data); + } else { + if ( + self.callDelegateMethod( + "dataServiceAccessControlFailedForOperations", + self, + failedOperations + ) + ) { + iFailOperation = self._dispatchTransactionFailedOperationFor( + transactionOperation, + new Error("Unauthorized") + ); + reject(iFailOperation.data); + } else { + resolve(true); + } + } + } + } + } catch (error) { + iFailOperation = self._dispatchTransactionFailedOperationFor(transactionOperation, error); + reject(error); + } + }); + } + }, + }, + + capturePerformTransactionOperation: { + value: function (performTransactionOperation) { + return this.performsAccessControlForTransactionOperation(performTransactionOperation); + }, + }, + + captureCreateOperation: { + value: function (readOperation) {}, + }, + + accessPoliciesForDataOperation: { + value: function (dataOperation) { + return this.accessPolicies; + + if (!this.__accessPoliciesByObjectDescriptor) { + this._registerAccessPoliciesByObjectDescriptor(this._accessPolicies); + } + + var accessPolicies = this._accessPoliciesByObjectDescriptor.get(dataOperation.target); + + return accessPolicies ? accessPolicies : null; + }, + }, + /** + * Assess wether a DataService (and it's children data services can perform an operation. + * + * Implemented by default by delegating to DataAccessPolicy + * @method + * @argument {DataOperation} authorizeConnectionOperation + * @returns {Promise} - A promise fulfilled with a boolean value. + * + */ + + //isDataOperationAuthorized: + isDataOperationAuthorized: { + value: function (dataOperation) { + return dataOperation.isAuthorized; + }, + }, + + authorizesDataOperationsWithoutAccessPolicy: { + value: false, + }, + + _evaluatedAccessPoliciesByDataOperation: { + value: new WeakMap(), + }, + evaluateAccessPoliciesForDataOperation: { + value: function (dataOperation) { + var evaluationResult = this._evaluatedAccessPoliciesByDataOperation.get(dataOperation); + + if (!evaluationResult) { + evaluationResult = this._evaluateAccessPoliciesForDataOperation(dataOperation); + + this._evaluatedAccessPoliciesByDataOperation.set(dataOperation, evaluationResult); + } + + return evaluationResult; + }, + }, + + _evaluateAccessPoliciesForDataOperation: { + value: function (dataOperation) { + //console.log("evaluateAccessPoliciesForDataOperation "+dataOperation.type+" "+dataOperation.target.name,dataOperation); + + // var shouldEvaluateAccessPoliciesForDataOperation = this.callDelegateMethod("dataServiceShouldEvaluateAccessPoliciesForDataOperation", this, dataOperation); + + // if(shouldEvaluateAccessPoliciesForDataOperation === undefined) { + // shouldEvaluateAccessPoliciesForDataOperation = dataOperation.clientId && !this.currentEnvironment.isNode; + // } + //Let's try that if an operation is coming from inside the DataWorker, it's authorized. + // if(!shouldEvaluateAccessPoliciesForDataOperation) { + if (!dataOperation.clientId && this.currentEnvironment.isNode) { + return Promise.resolve((dataOperation.isAuthorized = true)); + } else { + var accessPolicies = this.accessPoliciesForDataOperation(dataOperation); + + if (!accessPolicies || (accessPolicies && accessPolicies.length === 0)) { + console.log( + "accessPolicies: this.authorizesDataOperationsWithoutAccessPolicy is ", + this.authorizesDataOperationsWithoutAccessPolicy + ); + return Promise.resolve( + (dataOperation.isAuthorized = this.authorizesDataOperationsWithoutAccessPolicy) + ); + } else { + var i, + countI, + iAccessPolicy, + iAccessPolicyEvaluation, + iAccessPolicyEvaluationPromises, + self = this; + + for (i = 0, countI = accessPolicies ? accessPolicies.length : 0; i < countI; i++) { + iAccessPolicy = accessPolicies[i]; + iAccessPolicyEvaluation = iAccessPolicy.evaluate(dataOperation); + + if (this._isAsync(iAccessPolicyEvaluation)) { + (iAccessPolicyEvaluationPromises || (iAccessPolicyEvaluationPromises = [])).push( + iAccessPolicyEvaluation + ); + } + + /* If sync so far... */ + if (!iAccessPolicyEvaluationPromises && !this.isDataOperationAuthorized(dataOperation)) { + return Promise.resolve(false); + } + } + + if (iAccessPolicyEvaluationPromises && iAccessPolicyEvaluationPromises.length > 0) { + return Promise.all(iAccessPolicyEvaluationPromises).then(function () { + return self.isDataOperationAuthorized(dataOperation); + }); + } else { + //whatever the rules do, they set a state on the dataOperation, so nothing to resolve. + return Promise.resolve(this.isDataOperationAuthorized(dataOperation)); + } + } + } + }, + }, + + /** + * Answers wether logged-in application user can create a DataObject. + * + * Services overriding the (plural) + * @method + * @argument {ObjectDescriptor} dataObjectDescriptor + * @returns {Promise} - A promise fulfilled with a boolean value. + * + */ + canUserCreateDataObject: { + value: function (dataObjectDescriptor) { + /* + TODO: implement by leveraging Expression Based Access Control mapping/rules if available + */ + return Promise.resolve(true); + }, + }, + + /** + * Answers wether logged-in application user can edit/update? a DataObject. + * + * Services overriding the (plural) + * @method + * @argument {Object} dataObject + * @returns {Promise} - A promise fulfilled with a boolean value. + * + */ + canUserEditDataObject: { + value: function (dataObject) { + /* + return this.canPerfomDataOperation(DataOperation.toUpdateDataObject(dataObject)); + */ + return Promise.resolve(true); + }, + }, + canUserDeleteDataObject: { + value: function (dataObject) { + /* + return this.canPerfomDataOperation(DataOperation.toDeleteDataObject(dataObject)); + */ + return Promise.resolve(true); + }, + }, + + objectStoreCreateOperationForObjectDescriptor: { + value: function (objectDescriptor) { + var iOperation = new DataOperation(); + + iOperation.type = DataOperation.Type.CreateOperation; + iOperation.data = objectDescriptor; + iOperation.target = ObjectStoreDescriptor; + iOperation.rawDataService = this; + + return iOperation; + }, + }, + + objectPropertyStoreCreateOperationForPropertyDescriptor: { + value: function (propertyDescriptor, objectDescriptor) { + var iOperation = new DataOperation(); + + iOperation.type = DataOperation.Type.CreateOperation; + iOperation.data = { + propertyDescriptor: propertyDescriptor, + objectDescriptor: objectDescriptor, + }; + iOperation.target = ObjectPropertyStoreDescriptor; + iOperation.rawDataService = this; + + return iOperation; + }, + }, + + // responseOperationForCreateStorageOperation: { + // value: function (createOperation, err, data) { + + // } + // }, + + /* + WIP: now that we use the ObjectStore object descriptor as the target for objectstore creation, + we can clean up. However, since the objectDescriptor is now the data of the CreateCompletedOperation, + if it fails, the data of the CreateFailedOperation would be the error. Right now, we just return the error + from the raw layer, but we can and should create our own Erro so we can pass the objectDescriptor + */ + handleObjectStoreCreateCompletedOperation: { + value: function (operation) { + if (operation.rawDataService === this) { + this._objectDescriptorStoreExistsCache.set(operation.data, true); + operation.resolveCompletionPromise?.(operation); + } + }, + }, + + handleObjectStoreCreateFailedOperation: { + value: function (operation) { + if (operation.rawDataService === this) { + this._objectDescriptorStoreExistsCache.set(operation.data.objectDescriptor, operation.data); + operation.rejectCompletionPromise?.(operation); + } + }, + }, + + registerForObjectStoreCreateOperation: { + value: function () { + this.addEventListener(DataOperation.Type.CreateCompletedOperation, this, false); + this.addEventListener(DataOperation.Type.CreateFailedOperation, this, false); + }, + }, + unregisterForObjectStoreCreateOperation: { + value: function () { + this.removeEventListener(DataOperation.Type.CreateCompletedOperation, this, false); + this.removeEventListener(DataOperation.Type.CreateFailedOperation, this, false); + }, + }, + + createObjectStoreForObjectDescriptor: { + value: function (objectDescriptor) { + //console.log("create "+objectDescriptor.name); + var iOperation = this.createObjectStoreOperationForObjectDescriptor(objectDescriptor), + self = this; + + var createPromise = new Promise(function (resolve, reject) { + iOperation.resolveCompletionPromise = resolve; + iOperation.rejectCompletionPromise = reject; + + /* + The reason we're creating anonymous event handlers like this is that we're missing the semantic of having a "RawObjectDescriptor" + that should be the target of this operation, which would mean that we could implement handleCreateCompletedOperation() and + handleCreateFailedOperation() to handles propagation would deliver the relevant operations to us. + So if a subclass needed to do something to handle a failure, it would only have to override the handleCreateFailedOperation() method, + before callig super here that would then take care of the promise aspect. + */ + + // function createCompletedHandler(operation) { + // if(operation.referrerId === iOperation.id) { + // cleanupdHandlers(); + // self._objectDescriptorStoreExistsCache.set(objectDescriptor,true); + // resolve(operation); + // } + // }; + + // function createFailedHandler(operation) { + // if(operation.referrerId === iOperation.id) { + // cleanupdHandlers(); + // self._objectDescriptorStoreExistsCache.set(objectDescriptor,operation.data); + // reject(operation.data); + // } + // }; + + // function cleanupdHandlers() { + // self.removeEventListener(DataOperation.Type.CreateCompletedOperation,createCompletedHandler, false); + // self.removeEventListener(DataOperation.Type.createFailedHandler,createCompletedHandler, false); + // }; + + self.registerForObjectStoreCreateOperation(); + // self.addEventListener(DataOperation.Type.CreateCompletedOperation, createCompletedHandler, false); + // self.addEventListener(DataOperation.Type.CreateFailedOperation, createFailedHandler, false); + + iOperation.target.dispatchEvent(iOperation); + }); + + return createPromise; + }, + }, + + __objectDescriptorStoreExistsCache: { + value: undefined, + }, + _objectDescriptorStoreExistsCache: { + get: function () { + return this.__objectDescriptorStoreExistsCache || (this.__objectDescriptorStoreExistsCache = new Map()); + }, + }, + + createObjectStoreForObjectDescriptorIfNeeded: { + value: function (type) { + var objectDescriptor = this.objectDescriptorForType(type), + cachedValue = + this._objectDescriptorStoreExistsCache && + this._objectDescriptorStoreExistsCache.get(objectDescriptor), + self = this; + + if (cachedValue !== undefined) { + return Promise.is(cachedValue) ? cachedValue : Promise.resolve(cachedValue); + } else { + var query = DataQuery.withTypeAndCriteria(objectDescriptor), + queryPromise; + + //console.log("PlummingIntakeDataService _createObjectDescriptorStoreForTypeIfNeeded() --> fetchData to see if "+objectDescriptor.name+ " table exists"); + + query.sizeLimit = 1; + + queryPromise = this.fetchData(query).then( + (result) => { + //console.log("PlummingIntakeDataService _createObjectDescriptorStoreForTypeIfNeeded() --> "+objectDescriptor.name+ " table exists"); + this._objectDescriptorStoreExistsCache.set(objectDescriptor, false); + return false; + }, + (error) => { + if (error.name === DataOperationErrorNames.ObjectDescriptorStoreMissing) { + return self + .createObjectStoreForObjectDescriptor(objectDescriptor) + .then(() => { + // console.log("mainService.createObjectStoreForObjectDescriptor("+objectDescriptor.name+") COMPLETED!"); + // self._objectDescriptorStoreExistsCache.set(objectDescriptor,true); + return true; + }) + .catch((error) => { + console.error( + "mainService.createObjectStoreForObjectDescriptor(" + + objectDescriptor.name + + ") Error!", + error + ); + // self._objectDescriptorStoreExistsCache.set(objectDescriptor,error); + return Promise.reject(error); + }); + } else { + //That moved into createObjectStoreForObjectDescriptor(), we shouldn't keep that here + self._objectDescriptorStoreExistsCache.set(objectDescriptor, error); + return Promise.reject(error); + } + } + ); + + this._objectDescriptorStoreExistsCache.set(objectDescriptor, queryPromise); + + return queryPromise; + } + }, + }, + + /*************************************************************************** + * Utilities + */ + + /** + * A function that does nothing but returns null, useful for terminating + * a promise chain that needs to return null, as in the following code: + * + * var self = this; + * return this.fetchSomethingAsynchronously().then(function (data) { + * return self.doSomethingAsynchronously(data.part); + * }).then(this.nullFunction); + * + * @type {function} + */ + nullFunction: { + value: function () { + return null; + }, + }, + + /** + * A shared promise resolved with a value of + * `null`, useful for returning from methods like + * [fetchObjectProperty()]{@link DataService#fetchObjectProperty} + * when the requested data is already there. + * + * @type {external:Promise} + */ + nullPromise: { + get: function () { + if (!exports.DataService._nullPromise) { + exports.DataService._nullPromise = Promise.resolve(null); + } + return exports.DataService._nullPromise; + }, + }, + + _nullPromise: { + value: undefined, + }, + + /** + * @todo Document. + */ + emptyArrayPromise: { + get: function () { + if (!exports.DataService._emptyArrayPromise) { + exports.DataService._emptyArrayPromise = Promise.resolve([]); + } + return exports.DataService._emptyArrayPromise; + }, + }, + + _emptyArrayPromise: { + value: undefined, + }, + _emptyArray: { + value: Object.freeze([]), + }, + + /** + * A possibly shared promise resolved in the next cycle of the event loop + * or soon thereafter, at which point the current event handling will be + * complete. This is useful for services that need to buffer up actions so + * they're committed only once in a given event loop. + * + * @type {external:Promise} + */ + eventLoopPromise: { + get: function () { + var self = this; + if (!this._eventLoopPromise) { + this._eventLoopPromise = new Promise(function (resolve, reject) { + setTimeout(function () { + self._eventLoopPromise = undefined; + resolve(); + }, 0); + }); + } + return this._eventLoopPromise; + }, + }, + + /** + * Splice an array into another array. + * + * @method + * @argument {Array} array - The array to modify. + * @argument {Array} insert - The items to splice into that array. + * @argument {number} index - The index at which to splice those items, by + * default `0`. + * @argument {number} length - The number of items of the original array to + * replace with items from the spliced array, by + * default `array.length`. + */ + spliceWithArray: { + value: function (array, insert, index, length) { + index = index || 0; + length = length || length === 0 ? length : Infinity; + return insert ? array.splice.apply(array, [index, length].concat(insert)) : array.splice(index, length); + }, + }, + + /** + * Returns by default an array the Locale.systemLocale + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @property Array {Locale} + */ + + _userLocales: { + value: undefined, + }, + + userLocales: { + get: function () { + return this.isRootService + ? this._userLocales || ((this._userLocales = [Locale.systemLocale]) && this._userLocales) + : this.rootService.userLocales; + }, + set: function (value) { + if (value !== this._userLocales) { + this._userLocales = value; + } + }, + }, + + _userLocalesCriteria: { + value: undefined, + }, + + userLocalesCriteria: { + get: function () { + return this.isRootService + ? this._userLocalesCriteria || (this._userLocalesCriteria = this._createUserLocalesCriteria()) + : this.rootService.userLocalesCriteria; + }, + set: function (value) { + if (value !== this._userLocalesCriteria) { + this._userLocalesCriteria = value; + } + }, + }, + + _createUserLocalesCriteria: { + value: function () { + return new Criteria().initWithExpression("locales == $DataServiceUserLocales", { + DataServiceUserLocales: this.userLocales, + }); + }, + }, + + handleUserLocalesChange: { + value: function (value, key, object) { + this.userLocalesCriteria = this._createUserLocalesCriteria(); + }, + }, + + handleUserLocalesRangeChange: { + value: function (plus, minus) { + this.userLocalesCriteria = this._createUserLocalesCriteria(); + }, + }, + + /** + * Returns the locales for a specific object's locale. Default implementation + * returns userLocales. Subclasses can override to offer a per-object + * localization to be different than the session's one. + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @returns Array{Locale} + */ + + localesForObject: { + value: function (object) { + return object.locales || this.userLocales; + }, + }, + + /** + * Returns a criteria the locales for a specific object. Default implementation + * returns userLocales. Subclasses can override to offer a per-object + * localization to be different than the session's one. + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @returns Array{Locale} + */ + + localesCriteriaForObject: { + value: function (object) { + return this.userLocalesCriteria; + }, + }, + + /** + * The DataTrigger class used by the montage data stack + * + * @property {DataTrigger} + */ + + DataTrigger: { + value: DataTrigger, + }, + }, + /** @lends DataService */ { + /*************************************************************************** + * Service Hierarchy + */ + + /** + * A reference to the application's main service. + * + * Applications typically have one and only one + * [root service]{@link DataService#rootService} to which all data requests + * are sent, and this is called the application's main service. That service + * can in turn delegate handling of different types of data to child + * services specialized by type. + * + * This property will be set automatically if the {@link DataService} + * constructor is called and if the first service created is either the + * main service or a descendent of the main service. + * + * @type {DataService} + */ + mainService: { + get: function () { + if (this._mainService && this._mainService.parentService) { + this._mainService = this._mainService.rootService; + } + return this._mainService; + }, + set: function (service) { + //console.debug("mainService set: ",service); + this._mainService = service; + if (service) { + service.isMainService = true; + } + }, + }, + + /*************************************************************************** + * Authorization + */ + + AuthorizationPolicyType: { + value: AuthorizationPolicyType, + }, + + AuthorizationPolicy: { + value: AuthorizationPolicy, + }, + + authorizationManager: { + value: AuthorizationManager, + }, + + /*************************************************************************** + * Debugging + */ + + debugProperties: { + value: new Set(), + }, + } +); + +DataService.defineBinding("mainService", { "<-": "application.mainService", source: defaultEventManager }); + +//WARNING Shouldn't be a problem, but avoiding a potential require-cycle for now: +DataStream.DataService = exports.DataService; + + +/****** + * Tracks the changes to a collection between saves + */ + +function CollectionChanges() {} +Object.defineProperties(CollectionChanges.prototype, { + + + /*** + * The ids of the Change Events recorded in this object + * @property {Set} + ***/ + _trackedEventIds: { + get: function () { + return this.__trackedEventIds || (this.__trackedEventIds = new Set()); + } + }, + + /*** + * The ordered changes for the tracked collection. The structure of each item in this array will change + * based on the type of collection being tracked. + * + * Array: + * { + * index: integer + * addedValues: Array + * removedValues: Array + * } + * + * Set: (no current use case) + * { + * addedValues: Set + * removedValues: Set + * } + * + * Map: (no current use case) + * { + * addedValues: Array -- [{key: key, value: value}] + * removedValues: Array + * } + * @property {Array} + ***/ + changes: { + get: function () { + return this._changes || (this._changes = []); + } + }, + + hasChanges: { + get: function () { + + //FIXME: Use/test this._changes?.length > 0 + return this._changes && this._changes.length > 0; + } + }, + + /*** + * All values added to the collection + * @property {Set} + ***/ + addedValues: { + get: function () { + return this._addedValues || (this._addedValues = new Set()); + } + }, + + /*** + * Returns whether any values have been added or removed + * @property {Set} + ***/ + hasAddedOrRemovedValues: { + get: function () { + //FIXME: Use/test this._changes?.length > 0 + return (this._addedValues && this._addedValues.size > 0) || (this._removedValues && this._removedValues.size > 0); + } + }, + + /*** + * All values removed from the collection + * @property {Set} + ***/ + removedValues: { + get: function () { + return this._removedValues || (this._removedValues = new Set()); + } + }, + + /*** + * Add an entry to the changes array for a given change event + * + * This could be updated to: + * - Add values to addedValues + * - Add values to removedValues + * - Check the type of the property to add an object with the correct structure to changes + ***/ + trackChangeEvent: { + value: function (changeEvent) { + if (!this._trackedEventIds.has(changeEvent.id)) { + this._trackedEventIds.add(changeEvent.id); + this.changes.push({ + index: changeEvent.index, + addedValues: changeEvent.addedValues, + removedValues: changeEvent.removedValues + }) + } + } + } + +}) diff --git a/worker/data-worker.js b/worker/data-worker.js index b77fc64e0..f4d03b4e4 100644 --- a/worker/data-worker.js +++ b/worker/data-worker.js @@ -536,7 +536,7 @@ exports.DataWorker = Worker.specialize( /** @lends DataWorker.prototype */{ this.mainService.mergeDataObject(webSocketSession); - return this.mainService.saveChanges() + return this.mainService.saveChangesWithIdentity(identity) .then((result) => { return webSocketSession; })