From 9c17834de154d0e76439cc6bda524ad5c1b9985a Mon Sep 17 00:00:00 2001 From: tejaede Date: Tue, 3 Mar 2026 21:03:27 -0600 Subject: [PATCH 1/2] Update to add changes/deletions/creations directly to transaction --- data/model/transaction.js | 6 ++ data/service/data-service.js | 103 +++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/data/model/transaction.js b/data/model/transaction.js index 3c203a06f..511edb8ea 100644 --- a/data/model/transaction.js +++ b/data/model/transaction.js @@ -28,6 +28,12 @@ var Montage = require("../../core/core").Montage, * @type {Set} */ objectDescriptors: { + get: function () { + return new Set(this.objectDescriptorsWithChanges); + } + }, + + objectDescriptorsWithChanges: { value: undefined }, diff --git a/data/service/data-service.js b/data/service/data-service.js index 660002b8c..946442eb6 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -3305,16 +3305,18 @@ DataService.addClassProperties( }, objectDescriptorsWithChanges: { get: function () { - return this._objectDescriptorsWithChanges || (this._objectDescriptorsWithChanges = new CountedSet()); + // return this._objectDescriptorsWithChanges || (this._objectDescriptorsWithChanges = new CountedSet()); + return this._defaultTransaction.objectDescriptorsWithChanges; }, }, createdDataObjects: { get: function () { if (this.isRootService) { - if (!this._createdDataObjects) { - this._createdDataObjects = new Map(); - } - return this._createdDataObjects; + // if (!this._createdDataObjects) { + // this._createdDataObjects = new Map(); + // } + // return this._createdDataObjects; + return this._defaultTransaction.createdDataObjects; } else { return this.rootService.createdDataObjects; } @@ -3891,10 +3893,11 @@ DataService.addClassProperties( changedDataObjects: { get: function () { if (this.isRootService) { - if (!this._changedDataObjects) { - this._changedDataObjects = new Map(); - } - return this._changedDataObjects; + // if (!this._changedDataObjects) { + // this._changedDataObjects = new Map(); + // } + // return this._changedDataObjects; + return this._defaultTransaction.updatedDataObjects; } else { return this.rootService.changedDataObjects; } @@ -3965,7 +3968,8 @@ DataService.addClassProperties( dataObjectChanges: { get: function () { if (this.isRootService) { - return this._dataObjectChanges || (this._dataObjectChanges = new Map()); + // return this._dataObjectChanges || (this._dataObjectChanges = new Map()); + return this._defaultTransaction.dataObjectChanges; } else { return this.rootService.dataObjectChanges; } @@ -4816,8 +4820,9 @@ DataService.addClassProperties( deletedDataObjects: { get: function () { if (this.isRootService) { - this._deletedDataObjects = this._deletedDataObjects || new Map(); - return this._deletedDataObjects; + // this._deletedDataObjects = this._deletedDataObjects || new Map(); + // return this._deletedDataObjects; + return this._defaultTransaction.deletedDataObjects; } else { return this.rootService.deletedDataObjects; } @@ -5293,11 +5298,17 @@ DataService.addClassProperties( */ discardChanges: { value: function () { - this.createdDataObjects.clear(); - this.changedDataObjects.clear(); - this.deletedDataObjects.clear(); - this.dataObjectChanges.clear(); - this.objectDescriptorsWithChanges.clear(); + // this.createdDataObjects.clear(); + // this.changedDataObjects.clear(); + // this.deletedDataObjects.clear(); + // this.dataObjectChanges.clear(); + // this.objectDescriptorsWithChanges.clear(); + }, + }, + + _resetDefaultTransaction: { + value: function () { + this._defaultTransaction = this._createEmptyTransaction(); }, }, @@ -5309,6 +5320,30 @@ DataService.addClassProperties( value: false, }, + _defaultTransaction: { + get: function () { + if (!this.__defaultTransaction) { + this.__defaultTransaction = this._createEmptyTransaction(); + } + return this.__defaultTransaction; + }, + set: function (value) { + this.__defaultTransaction = value; + } + }, + + _createEmptyTransaction: { + value: function () { + let transaction = new Transaction(); + transaction.createdDataObjects = new Map(); + transaction.updatedDataObjects = new Map(); + transaction.deletedDataObjects = new Map(); + transaction.objectDescriptorsWithChanges = new CountedSet(); + transaction.dataObjectChanges = new Map(); + return transaction; + } + }, + saveChanges: { value: function () { //If nothing to do, we bail out as early as possible. @@ -5335,17 +5370,24 @@ DataService.addClassProperties( } } - var transaction = new Transaction(), - self = this, + // var transaction = new Transaction(), + var transaction = this._defaultTransaction, //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 - )); + // 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 + // )); + createdDataObjects = this.createdDataObjects, //Map + changedDataObjects = this.changedDataObjects, //Map + deletedDataObjects = this.deletedDataObjects, //Map + dataObjectChanges = this.dataObjectChanges; //Map + // objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( + // this.objectDescriptorsWithChanges + // )); //console.log("saveChanges: transaction-"+this.identifier, transaction); console.log( @@ -5357,16 +5399,9 @@ DataService.addClassProperties( 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(); - // this.createdDataObjects.clear(); - // this.changedDataObjects.clear(); - // this.deletedDataObjects.clear(); - // this.dataObjectChanges.clear(); - // this.objectDescriptorsWithChanges.clear(); + this._resetDefaultTransaction(); return this._saveChangesForTransaction(transaction); }, From 74bcbc7856c0f2cbc29a7fae12c2b25c09cae3f6 Mon Sep 17 00:00:00 2001 From: tejaede Date: Tue, 10 Mar 2026 17:31:20 -0500 Subject: [PATCH 2/2] Move change tracking to transactions --- data/model/transaction.js | 412 +++++++++++++++++++++++++++++++++- data/service/data-service.js | 225 ++++++++++++------- test/spec/data/transaction.js | 14 ++ 3 files changed, 560 insertions(+), 91 deletions(-) create mode 100644 test/spec/data/transaction.js diff --git a/data/model/transaction.js b/data/model/transaction.js index 511edb8ea..6bb300776 100644 --- a/data/model/transaction.js +++ b/data/model/transaction.js @@ -1,6 +1,7 @@ var Montage = require("../../core/core").Montage, uuid = require("../../core/uuid"), DataService = require("../service/data-service").DataService, + CountedSet = require("core/counted-set").CountedSet, Transaction; /** @@ -18,6 +19,13 @@ var Montage = require("../../core/core").Montage, } }, + init: { + value: function (service) { + this.service = service; + return this; + } + }, + identifier: { value: undefined }, @@ -34,7 +42,9 @@ var Montage = require("../../core/core").Montage, }, objectDescriptorsWithChanges: { - value: undefined + get: function () { + return this._objectDescriptorsWithChanges || (this._objectDescriptorsWithChanges = new CountedSet()); + } }, _completionPromiseFunctionsByParticipant: { @@ -186,7 +196,18 @@ var Montage = require("../../core/core").Montage, * @type {Map} */ createdDataObjects: { - value: undefined + get: function () { + if (!this._createdDataObjects) { + this._createdDataObjects = new Map(); + var self = this; + this._createdDataObjects.addMapChangeListener(function (value, key) { + value.addRangeChangeListener(function (dataObject) { + self.service.registerTransactionForObject(dataObject, self); + }); + }); + } + return this._createdDataObjects; + } }, /** @@ -195,7 +216,24 @@ var Montage = require("../../core/core").Montage, * @type {Map} */ updatedDataObjects: { - value: undefined + get: function () { + if (!this._updatedDataObjects) { + this._updatedDataObjects = new Map(); + var self = this; + this._updatedDataObjects.addMapChangeListener(function (value, key) { + value.addRangeChangeListener(function (dataObject) { + self.service.registerTransactionForObject(dataObject, self); + }); + }); + } + return this._updatedDataObjects; + } + }, + + changedDataObjects: { + get: function () { + return this.updatedDataObjects; + } }, /** @@ -209,13 +247,49 @@ var Montage = require("../../core/core").Montage, }, + /** + * 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; + }, + }, + /** * 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 + get: function () { + if (!this._dataObjectChanges) { + this._dataObjectChanges = new Map(); + var self = this; + this._dataObjectChanges.addMapChangeListener(function (dataObject, key) { + // value.addMapChangeListener(function (changesSet, dataObject) { + self.service.registerTransactionForObject(dataObject, self); + // }); + }); + } + return this._dataObjectChanges; + } + }, + + changesForDataObject: { + value: function (dataObject) { + return this.dataObjectChanges.get(dataObject) || this._buildChangesForDataObject(dataObject); + }, }, /** @@ -224,7 +298,18 @@ var Montage = require("../../core/core").Montage, * @type {Map} */ deletedDataObjects: { - value: undefined + get: function () { + if (!this._deletedDataObjects) { + this._deletedDataObjects = new Map(); + var self = this; + this._deletedDataObjects.addMapChangeListener(function (value, key) { + value.addRangeChangeListener(function (dataObject) { + self.service.registerTransactionForObject(dataObject, self); + }); + }); + } + return this._deletedDataObjects || (this._deletedDataObjects = new Map()); + } }, /** @@ -310,5 +395,322 @@ var Montage = require("../../core/core").Montage, }, + //Track Object Changes + registerDataObjectChangesFromEvent: { + value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { + var dataObject = changeEvent.target, + key = changeEvent.key, + objectDescriptor = this.service.objectDescriptorForObject(dataObject), + propertyDescriptor = objectDescriptor.propertyDescriptorForName(key); + // isDataObjectBeingMapped = this._objectsBeingMapped.has(dataObject); + + //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; + } + + this._registerDataObjectChangesFromEvent( + changeEvent, + shouldTrackChangesWhileBeingMapped + ); + }, + }, + + _registerDataObjectChangesFromEvent: { + value: function ( + changeEvent, + shouldTrackChangesWhileBeingMapped + ) { + var dataObject = changeEvent.target, + isCreatedObject = this.createdDataObjects.has(dataObject) || this.service.isObjectCreated(dataObject), + key = changeEvent.key, + keyValue = changeEvent.keyValue, + addedValues = changeEvent.addedValues, + removedValues = changeEvent.removedValues, + //FIX ME -- Remove reference to private property + isDataObjectBeingMapped = this.service._objectsBeingMapped.has(dataObject), + changesForDataObject = 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.updatedDataObjects.add(dataObject); + 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. + + + */ + + //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), + i, + countI; + + if (!manyChanges) { + manyChanges = {}; + manyChanges.index = changeEvent.index; + changesForDataObject.set(key, manyChanges); + } + + //Not sure if we should consider evaluating added values regarded + //removed ones, one could be added and later removed. + //We later need to convert these into dataIdentifers, we could avoid a loop later + //doing so right here. + + /* + Benoit 1/8/26 - Got a use-case of a swap: same values in addedValues and removedValues. + But with processing removedValues being the last, it would empty the array... + + So removedValues needs to be handles first, and then addedValues second + */ + if (removedValues) { + /* + In this case, the array already contains the added value and we'll save it all anyway. So we just propagate. + If the change is triggered by resolving properties by the framewok itself, then isDataObjectBeingMapped is true, and we don't want to register any of it as a change + */ + if (Array.isArray(manyChanges) && (isCreatedObject || isDataObjectBeingMapped)) { + //noop + } else { + var registeredRemovedValues = manyChanges.removedValuesSet; + if (!registeredRemovedValues) { + if (!isDataObjectBeingMapped) { + manyChanges.removedValues = removedValues; + manyChanges.removedValuesSet = registeredRemovedValues = new Set(removedValues); + } + } else { + for (i = 0, countI = removedValues.length; i < countI; i++) { + if (!isDataObjectBeingMapped) { + registeredRemovedValues.add(removedValues[i]); + } + } + } + } + /* + Work on local graph integrity. When objects are disassociated, it could mean some deletions may happen bases on delete rules. + App side goal is to maintain the App graph, server's side is to maintain database integrity. Both needs to act on delete rules: + - get object's descriptor + - get PropertyDescriptor from key + - get PropertyDescriptor's .deleteRule + deleteRule can be: + - DeleteRule.NULLIFY + - DeleteRule.CASCADE + - DeleteRule.DENY + - DeleteRule.IGNORE + */ + + //,,,,,TODO + } + + if (addedValues) { + /* + In this case, the array already contains the added value and we'll save it all anyway. So we just propagate. + */ + if (Array.isArray(manyChanges) && (isCreatedObject || isDataObjectBeingMapped)) { + //noop + } else { + var registeredAddedValues = manyChanges.addedValuesSet; + if (!registeredAddedValues) { + /* + FIXME: we ended up here with manyChanges being an array, containing the same value as addedValues. And we end up setting addedValues property on that array. So let's correct it. We might not want to track toMany as set at all, and just stick to added /remove. This might happens on remove as well, we need to check further. + */ + if (Array.isArray(manyChanges) && manyChanges.equals(addedValues)) { + manyChanges = {}; + manyChanges.index = changeEvent.index; + changesForDataObject.set(key, manyChanges); + } + + if (!isDataObjectBeingMapped) { + manyChanges.addedValues = addedValues; + manyChanges.addedValuesSet = registeredAddedValues = new Set(addedValues); + } + } + } + } + + } + } + }, + + registerCreatedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.service.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); + } + }, + + unregisterCreatedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.objectDescriptorForObject(dataObject), + value = this.createdDataObjects.get(objectDescriptor); + if (value) { + value.delete(dataObject); + if (value.size === 0) { + this.createdDataObjects.delete(objectDescriptor); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + } + }, + }, + + registerChangedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.service.objectDescriptorForObject(dataObject), + updatedDataObjects, + value; + + if (this.createdDataObjects.has(dataObject) || this.service.isObjectCreated(dataObject)) { + console.warn( + `DataService can't register a new object (${objectDescriptor.name}) in updatedDataObjects` + ); + return; + } + + updatedDataObjects = this.updatedDataObjects; + value = updatedDataObjects.get(objectDescriptor); + + if (!value) { + updatedDataObjects.set(objectDescriptor, (value = new Set())); + } + value.add(dataObject); + this.objectDescriptorsWithChanges.add(objectDescriptor); + }, + }, + + unregisterChangedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.service.objectDescriptorForObject(dataObject), + value = this.updatedDataObjects.get(objectDescriptor); + + if (value) { + value.delete(dataObject); + if (value.size === 0) { + this.updatedDataObjects.delete(objectDescriptor); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + } + }, + }, + + registerDeletedDataObject: { + value: function (dataObject) { + var objectDescriptor = this.service.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); + if (value.size === 0) { + this.deletedDataObjects.delete(objectDescriptor); + this.objectDescriptorsWithChanges.delete(objectDescriptor); + } + + } + } + }, }); diff --git a/data/service/data-service.js b/data/service/data-service.js index 946442eb6..d0458617c 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -3305,6 +3305,7 @@ DataService.addClassProperties( }, objectDescriptorsWithChanges: { get: function () { + //TODO Remove Leaving here in case RawDataServices access it // return this._objectDescriptorsWithChanges || (this._objectDescriptorsWithChanges = new CountedSet()); return this._defaultTransaction.objectDescriptorsWithChanges; }, @@ -3312,10 +3313,7 @@ DataService.addClassProperties( createdDataObjects: { get: function () { if (this.isRootService) { - // if (!this._createdDataObjects) { - // this._createdDataObjects = new Map(); - // } - // return this._createdDataObjects; + //TODO Remove Leaving here in case RawDataServices access it return this._defaultTransaction.createdDataObjects; } else { return this.rootService.createdDataObjects; @@ -3323,23 +3321,26 @@ DataService.addClassProperties( }, }, - registerCreatedDataObject: { - value: function (dataObject) { - var objectDescriptor = this.objectDescriptorForObject(dataObject), - createdDataObjects = this.createdDataObjects, - value = createdDataObjects.get(objectDescriptor); - if (!value) { - createdDataObjects.set(objectDescriptor, (value = new Set())); - } + _transactionsByObject: { + get: function () { + return this.__transactionsByObject || (this.__transactionsByObject = new Map()); + } + }, - /* - 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); + registerTransactionForObject: { + value: function (object, transaction) { + if (this._transactionsByObject.has(object) && this._transactionsByObject.get(object) !== transaction) { + console.warn(`[DataService] Transaction is already registered for object ${object.objectDescriptor.name}/${object.dataIdentifier.primaryKey}`); + return; + } + this._transactionsByObject.set(object, transaction); + } + }, - value.add(dataObject); - this.objectDescriptorsWithChanges.add(objectDescriptor); + registerCreatedDataObject: { + value: function (dataObject) { + let transaction = this._transactionForObject(dataObject); + transaction.registerCreatedDataObject(dataObject); this.dispatchDataEventTypeForObject(DataEvent.create, dataObject); }, @@ -3347,12 +3348,8 @@ DataService.addClassProperties( unregisterCreatedDataObject: { value: function (dataObject) { - var objectDescriptor = this.objectDescriptorForObject(dataObject), - value = this.createdDataObjects.get(objectDescriptor); - if (value) { - value.delete(dataObject); - this.objectDescriptorsWithChanges.delete(objectDescriptor); - } + let transaction = this._transactionForObject(dataObject); + transaction.unregisterCreatedDataObject(dataObject); }, }, @@ -3556,12 +3553,13 @@ DataService.addClassProperties( value: function (dataObject, delegate, delegateObjectToMerge, _promises, _isRoot, _mergingDataObjects) { if (!delegateObjectToMerge || (delegateObjectToMerge && dataObject === delegateObjectToMerge)) { let objectDescriptor = this.objectDescriptorForObject(dataObject), - createdDataObjects = this.createdDataObjects, + transaction = this._transactionForObject(dataObject), + createdDataObjects = transaction.createdDataObjects, value = createdDataObjects.get(objectDescriptor); if (!value) { createdDataObjects.set(objectDescriptor, (value = new Set())); - this.objectDescriptorsWithChanges.add(objectDescriptor); + transaction.objectDescriptorsWithChanges.add(objectDescriptor); } if (!value.has(dataObject)) { @@ -3799,7 +3797,8 @@ DataService.addClassProperties( isObjectCreated: { value: function (object) { var objectDescriptor = this.objectDescriptorForObject(object), - createdDataObjects = this.createdDataObjects.get(objectDescriptor), + transaction = this._transactionForObject(object), + createdDataObjects = transaction.createdDataObjects.get(objectDescriptor), isObjectCreated = createdDataObjects && createdDataObjects.has(object); if (!isObjectCreated) { @@ -3905,43 +3904,22 @@ DataService.addClassProperties( }, registerChangedDataObject: { value: function (dataObject) { - 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 = this.changedDataObjects; - value = changedDataObjects.get(objectDescriptor); - - if (!value) { - changedDataObjects.set(objectDescriptor, (value = new Set())); - } - value.add(dataObject); - this.objectDescriptorsWithChanges.add(objectDescriptor); + let transaction = this._transactionForObject(dataObject); + transaction.registerChangedDataObject(dataObject); }, }, isObjectChanged: { value: function (dataObject) { - return this.changedDataObjects.get(dataObject.objectDescriptor)?.has(dataObject); + let transaction = this._transactionForObject(dataObject); + return transaction.changedDataObjects.get(dataObject.objectDescriptor)?.has(dataObject); }, }, unregisterChangedDataObject: { value: function (dataObject) { - var objectDescriptor = this.objectDescriptorForObject(dataObject), - value = this.changedDataObjects.get(objectDescriptor); - - if (value) { - value.delete(dataObject); - this.objectDescriptorsWithChanges.delete(objectDescriptor); - } + let transaction = this._transactionForObject(dataObject); + transaction.unregisterChangedDataObject(dataObject); }, }, @@ -4457,7 +4435,94 @@ DataService.addClassProperties( value: queueMicrotask.debounceWithDelay(500), }, + _transactionForObject: { + value: function (dataObject) { + return this._transactionsByObject.has(dataObject) ? this._transactionsByObject.get(dataObject) : this._defaultTransaction; + } + }, + registerDataObjectChangesFromEvent: { + value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { + let dataObject = changeEvent.target, + transaction = this._transactionForObject(dataObject), + objectDescriptor = this.objectDescriptorForObject(dataObject), + propertyDescriptor = objectDescriptor.propertyDescriptorForName(changeEvent.key), + isDataObjectBeingMapped = this._objectsBeingMapped.has(dataObject); + + + transaction.registerDataObjectChangesFromEvent(changeEvent, shouldTrackChangesWhileBeingMapped); + + if (propertyDescriptor.definition) { + return; + } + + if (!isDataObjectBeingMapped && this.autosaves && transaction === this._defaultTransaction) { + //this.isAutosaveScheduled = true; + this.debouncedQueueMicrotaskWithDelay(() => { + 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 + ); + } + + } + }, + + _og_registerDataObjectChangesFromEvent: { value: function (changeEvent, shouldTrackChangesWhileBeingMapped) { var dataObject = changeEvent.target, key = changeEvent.key, @@ -4613,9 +4678,9 @@ DataService.addClassProperties( key !== "length" && /* new for blocking re-entrant */ changesForDataObject.get(key) !== keyValue ) { - if (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped) { - changesForDataObject.set(key, keyValue); - } + // if (!isDataObjectBeingMapped || shouldTrackChangesWhileBeingMapped) { + // changesForDataObject.set(key, keyValue); + // } //Now set the inverse if any if (inversePropertyDescriptor) { @@ -4835,14 +4900,8 @@ DataService.addClassProperties( 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); + var transaction = this._transactionForObject(dataObject); + transaction.registerDeletedDataObject(dataObject); }, }, @@ -4854,12 +4913,8 @@ DataService.addClassProperties( unregisterDeletedDataObject: { value: function (dataObject) { - var objectDescriptor = this.objectDescriptorForObject(dataObject), - value = this.deletedDataObjects.get(objectDescriptor); - if (value) { - value.delete(dataObject); - this.objectDescriptorsWithChanges.delete(objectDescriptor); - } + var transaction = this._transactionForObject(dataObject); + transaction.unregisterDeletedDataObject(dataObject); }, }, @@ -5334,13 +5389,7 @@ DataService.addClassProperties( _createEmptyTransaction: { value: function () { - let transaction = new Transaction(); - transaction.createdDataObjects = new Map(); - transaction.updatedDataObjects = new Map(); - transaction.deletedDataObjects = new Map(); - transaction.objectDescriptorsWithChanges = new CountedSet(); - transaction.dataObjectChanges = new Map(); - return transaction; + return (new Transaction()).init(this); } }, @@ -5348,9 +5397,9 @@ DataService.addClassProperties( value: function () { //If nothing to do, we bail out as early as possible. if ( - this.createdDataObjects.size === 0 && - this.changedDataObjects.size === 0 && - this.deletedDataObjects.size === 0 + this._defaultTransaction.createdDataObjects.size === 0 && + this._defaultTransaction.changedDataObjects.size === 0 && + this._defaultTransaction.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, @@ -5381,10 +5430,10 @@ DataService.addClassProperties( // objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( // this.objectDescriptorsWithChanges // )); - createdDataObjects = this.createdDataObjects, //Map - changedDataObjects = this.changedDataObjects, //Map - deletedDataObjects = this.deletedDataObjects, //Map - dataObjectChanges = this.dataObjectChanges; //Map + createdDataObjects = transaction.createdDataObjects, //Map + changedDataObjects = transaction.changedDataObjects, //Map + deletedDataObjects = transaction.deletedDataObjects, //Map + dataObjectChanges = transaction.dataObjectChanges; //Map // objectDescriptorsWithChanges = (transaction.objectDescriptors = new Set( // this.objectDescriptorsWithChanges // )); @@ -5536,6 +5585,10 @@ DataService.addClassProperties( for (let i = 0, countI = pendingTransactions.length; i < countI; i++) { let iPendingTransaction = pendingTransactions[i]; + if (iPendingTransaction === transaction) { + continue; + } + if (iPendingTransaction.createdDataObjects.has(iObjectDescriptor)) { let createdDataObjects = iPendingTransaction.createdDataObjects.get(iObjectDescriptor); diff --git a/test/spec/data/transaction.js b/test/spec/data/transaction.js new file mode 100644 index 000000000..d044863d5 --- /dev/null +++ b/test/spec/data/transaction.js @@ -0,0 +1,14 @@ +var DataService = require("mod/data/service/data-service").DataService, + DataObjectDescriptor = require("mod/data/model/data-object-descriptor").DataObjectDescriptor, + ModuleObjectDescriptor = require("mod/core/meta/module-object-descriptor").ModuleObjectDescriptor, + ModuleReference = require("mod/core/module-reference").ModuleReference, + RawDataService = require("mod/data/service/raw-data-service").RawDataService, + defaultEventManager = require("mod/core/event/event-manager").defaultEventManager; + +const AnimatedMovieDescriptor = require("spec/data/logic/model/animated-movie.mjson").montageObject; +const CategoyDescriptor = require("spec/data/logic/model/category.mjson").montageObject; +const movieDescriptor = require("spec/data/logic/model/movie.mjson").montageObject; + +describe("A Transaction", function () { + +}); \ No newline at end of file