Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API] Changes to mutation API #3483

Merged
merged 45 commits into from
Jan 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
76a5d57
Migrating code to master
akhenry Oct 29, 2020
55bb43b
Removed * listener from BrowseBar
akhenry Oct 29, 2020
6379a78
Removed * listener from LAD Tables
akhenry Oct 29, 2020
05f874b
Removed * listener from ObjectView
akhenry Oct 29, 2020
d832b10
Make object reactive
akhenry Oct 29, 2020
778e078
Removed * listeners from selection API, composition API, and elements…
akhenry Oct 29, 2020
5b9282b
Removed * listener from objectLabel
akhenry Oct 29, 2020
1e61341
Fix issue with sub objects not showing in display layouts
akhenry Oct 30, 2020
f558ab5
Revert change to isEditingAllowed. It's checking the parent browse ob…
akhenry Oct 30, 2020
fb84ae6
Fixed bug in sub object views
akhenry Oct 30, 2020
4cf5e69
Removed * listeners from Display Layouts
akhenry Oct 30, 2020
7cdafa9
Added some documentation
akhenry Oct 30, 2020
1ea5a46
Fixed linting errors
akhenry Oct 30, 2020
7524f43
Fixed broken tests
akhenry Oct 31, 2020
25664f3
Added test specs
akhenry Oct 31, 2020
4d601f2
Moved destroy method to Object API
akhenry Nov 11, 2020
223653c
Switch ObjectAPI to ES6 module
akhenry Nov 11, 2020
74373bc
bump karma coverage threshold
akhenry Nov 11, 2020
52df255
Removed listener cleanup that is no longer needed
akhenry Nov 11, 2020
32f41dc
Merged from master
akhenry Nov 11, 2020
8701cff
Merged from Master
akhenry Nov 24, 2020
4f58808
Fixed test that was failing due to immutable object
akhenry Nov 25, 2020
9098a8a
Fixed bug in mutation API where original object was not being mutated…
akhenry Nov 25, 2020
2208f30
Fixed failing unit test
akhenry Nov 25, 2020
cfda4ae
Fixed linting errors
akhenry Nov 25, 2020
6ede1a3
Revert coverage threshold
akhenry Nov 25, 2020
2d076b4
Fixed bug in display layouts
akhenry Nov 25, 2020
abf0114
Fixed bug where selection items without domain object throws error
akhenry Nov 25, 2020
c546598
Use beforeDestroy instead of destroyed()
akhenry Dec 21, 2020
92f0eed
Remove redundant if statement
akhenry Dec 21, 2020
5adf04b
Explicit check for object existence
akhenry Dec 21, 2020
8982894
Fix spelling mistake
akhenry Dec 21, 2020
b04a1da
getMutable should only return a MutableDomainObject, and should throw…
akhenry Dec 22, 2020
a7c0030
Merged from master
akhenry Dec 22, 2020
d8c17e8
Fixed broken test
akhenry Dec 22, 2020
db1a9bf
Fixed lint warning in Vue file
akhenry Dec 22, 2020
c332daa
Move mutability check
akhenry Dec 22, 2020
9f439e5
MutableObject event should be instance-local
akhenry Dec 22, 2020
5f4921d
Only wrap path objects if mutable
akhenry Dec 22, 2020
6db50ca
Only get mutables if object is mutable
akhenry Dec 22, 2020
891f9ea
Only attempt to destroy objects if they are mutable
akhenry Dec 22, 2020
f7ea590
Restore default object to ObjectView for tabs
akhenry Dec 22, 2020
0a7782c
Fixed bug in deleted property detection
akhenry Dec 23, 2020
834f0c2
Merge branch 'master' into mutation-update
shefalijoshi Jan 4, 2021
3186b23
Merged from Master
akhenry Jan 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ define(
// is also invoked during the create process which should be allowed, // is also invoked during the create process which should be allowed,
// because it may be saved elsewhere // because it may be saved elsewhere
if ((key === 'edit' && category === 'view-control') || key === 'properties') { if ((key === 'edit' && category === 'view-control') || key === 'properties') {
let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId()); let identifier = this.openmct.objects.parseKeyString(domainObject.getId());


return this.openmct.objects.isPersistable(newStyleObject); return this.openmct.objects.isPersistable(identifier);
} }


return true; return true;
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ define(
); );


mockObjectAPI = jasmine.createSpyObj('objectAPI', [ mockObjectAPI = jasmine.createSpyObj('objectAPI', [
'isPersistable' 'isPersistable',
'parseKeyString'
]); ]);


mockAPI = { mockAPI = {
Expand Down
4 changes: 2 additions & 2 deletions platform/containment/src/PersistableCompositionPolicy.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ define(
// prevents editing of objects that cannot be persisted, so we can assume that this // prevents editing of objects that cannot be persisted, so we can assume that this
// is a new object. // is a new object.
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) { if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
let newStyleObject = objectUtils.toNewFormat(parent, parent.getId()); let identifier = this.openmct.objects.parseKeyString(parent.getId());


return this.openmct.objects.isPersistable(newStyleObject); return this.openmct.objects.isPersistable(identifier);
} }


return true; return true;
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ define(


beforeEach(function () { beforeEach(function () {
objectAPI = jasmine.createSpyObj('objectsAPI', [ objectAPI = jasmine.createSpyObj('objectsAPI', [
'isPersistable' 'isPersistable',
'parseKeyString'
]); ]);


mockOpenMCT = { mockOpenMCT = {
Expand Down
4 changes: 2 additions & 2 deletions src/MCT.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objects * @name objects
*/ */
this.objects = new api.ObjectAPI(); this.objects = new api.ObjectAPI.default(this.types);


/** /**
* An interface for retrieving and interpreting telemetry data associated * An interface for retrieving and interpreting telemetry data associated
Expand Down Expand Up @@ -371,7 +371,7 @@ define([
* MCT; if undefined, MCT will be run in the body of the document * MCT; if undefined, MCT will be run in the body of the document
*/ */
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) { MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (!this.plugins.DisplayLayout._installed) { if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({ this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget'] showAsView: ['summary-widget']
})); }));
Expand Down
1 change: 1 addition & 0 deletions src/adapter/services/LegacyObjectAPIInterceptor.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ define([
const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId()); const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
const keystring = utils.makeKeyString(newStyleObject.identifier); const keystring = utils.makeKeyString(newStyleObject.identifier);


this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
this.eventEmitter.emit(keystring + ":*", newStyleObject); this.eventEmitter.emit(keystring + ":*", newStyleObject);
this.eventEmitter.emit('mutation', newStyleObject); this.eventEmitter.emit('mutation', newStyleObject);
}.bind(this); }.bind(this);
Expand Down
4 changes: 4 additions & 0 deletions src/api/actions/ActionCollectionSpec.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ describe('The ActionCollection', () => {
} }
} }
]; ];
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
'create',
'update'
]));
mockView = { mockView = {
getViewContext: () => { getViewContext: () => {
return { return {
Expand Down
44 changes: 34 additions & 10 deletions src/api/composition/CompositionCollection.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ define([
}; };
this.onProviderAdd = this.onProviderAdd.bind(this); this.onProviderAdd = this.onProviderAdd.bind(this);
this.onProviderRemove = this.onProviderRemove.bind(this); this.onProviderRemove = this.onProviderRemove.bind(this);
this.mutables = {};

if (this.domainObject.isMutable) {
this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
});
unobserve();
});
}
} }


/** /**
Expand All @@ -75,10 +86,6 @@ define([
throw new Error('Event not supported by composition: ' + event); throw new Error('Event not supported by composition: ' + event);
} }


if (!this.mutationListener) {
this._synchronize();
}

if (this.provider.on && this.provider.off) { if (this.provider.on && this.provider.off) {
if (event === 'add') { if (event === 'add') {
this.provider.on( this.provider.on(
Expand Down Expand Up @@ -189,6 +196,13 @@ define([


this.provider.add(this.domainObject, child.identifier); this.provider.add(this.domainObject, child.identifier);
} else { } else {
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child)) {
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);

child = this.publicAPI.objects._toMutable(child);
this.mutables[keyString] = child;
}

this.emit('add', child); this.emit('add', child);
} }
}; };
Expand All @@ -202,6 +216,8 @@ define([
* @name load * @name load
*/ */
CompositionCollection.prototype.load = function () { CompositionCollection.prototype.load = function () {
this.cleanUpMutables();

return this.provider.load(this.domainObject) return this.provider.load(this.domainObject)
.then(function (children) { .then(function (children) {
return Promise.all(children.map((c) => this.publicAPI.objects.get(c))); return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
Expand Down Expand Up @@ -234,6 +250,14 @@ define([
if (!skipMutate) { if (!skipMutate) {
this.provider.remove(this.domainObject, child.identifier); this.provider.remove(this.domainObject, child.identifier);
} else { } else {
if (this.returnMutables) {
let keyString = this.publicAPI.objects.makeKeyString(child);
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
delete this.mutables[keyString];
}
}

this.emit('remove', child); this.emit('remove', child);
} }
}; };
Expand Down Expand Up @@ -281,12 +305,6 @@ define([
this.remove(child, true); this.remove(child, true);
}; };


CompositionCollection.prototype._synchronize = function () {
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
});
};

CompositionCollection.prototype._destroy = function () { CompositionCollection.prototype._destroy = function () {
if (this.mutationListener) { if (this.mutationListener) {
this.mutationListener(); this.mutationListener();
Expand All @@ -308,5 +326,11 @@ define([
}); });
}; };


CompositionCollection.prototype.cleanUpMutables = function () {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
});
};

return CompositionCollection; return CompositionCollection;
}); });
137 changes: 137 additions & 0 deletions src/api/objects/MutableDomainObject.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,137 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import utils from './object-utils.js';
import EventEmitter from 'EventEmitter';

const ANY_OBJECT_EVENT = 'mutation';

/**
* Wraps a domain object to keep its model synchronized with other instances of the same object.
*
* Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers
* should be careful to destroy MutableDomainObject in order to avoid memory leaks.
*
* All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except
* `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`
* will result in the creation of a new MutableDomainObject and you will be responsible for destroying it
* (via openmct.objects.destroy) when you're done with it.
*
* @typedef MutableDomainObject
* @memberof module:openmct
*/
class MutableDomainObject {
constructor(eventEmitter) {
Object.defineProperties(this, {
_globalEventEmitter: {
value: eventEmitter,
// Property should not be serialized
enumerable: false
},
_instanceEventEmitter: {
value: new EventEmitter(),
// Property should not be serialized
enumerable: false
},
_observers: {
value: [],
// Property should not be serialized
enumerable: false
},
isMutable: {
value: true,
// Property should not be serialized
enumerable: false
}
});
}
$observe(path, callback) {
let fullPath = qualifiedEventName(this, path);
let eventOff =
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);

this._globalEventEmitter.on(fullPath, callback);
this._observers.push(eventOff);

return eventOff;
}
$set(path, value) {
_.set(this, path, value);
_.set(this, 'modified', Date.now());

//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);

//Emit a general "any object" event
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);

//Emit events specific to properties affected
let parentPropertiesList = path.split('.');
for (let index = parentPropertiesList.length; index > 0; index--) {
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
}

//TODO: Emit events for listeners of child properties when parent changes.
// Do it at observer time - also register observers for parent attribute path.
}
$on(event, callback) {
this._instanceEventEmitter.on(event, callback);

return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
this._observers.forEach(observer => observer());
delete this._globalEventEmitter;
delete this._observers;
this._instanceEventEmitter.emit('$_destroy');
}

static createMutable(object, mutationTopic) {
let mutable = Object.create(new MutableDomainObject(mutationTopic));
Object.assign(mutable, object);

mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
});

return mutable;
}

static mutateObject(object, path, value) {
_.set(object, path, value);
_.set(object, 'modified', Date.now());
}
}

function qualifiedEventName(object, eventName) {
let keystring = utils.makeKeyString(object.identifier);

return [keystring, eventName].join(':');
}

export default MutableDomainObject;
Loading