Compare commits

...

1 Commits

Author SHA1 Message Date
Andrew Henry
5e1d558cf7 Finished observers 2022-07-25 10:43:49 -07:00
5 changed files with 246 additions and 73 deletions

View File

@@ -52,8 +52,8 @@ class MutableDomainObject {
// Property should not be serialized
enumerable: false
},
_observers: {
value: [],
_callbacksForPaths: {
value: {},
// Property should not be serialized
enumerable: false
},
@@ -64,15 +64,31 @@ class MutableDomainObject {
}
});
}
/**
* BRAND new approach
* - Register a listener on $_synchronize_model
* - The $_synchronize_model event provides the path. Figure out whether the mutated path is equal to, or a parent of the observed path.
* - If so, trigger callback with new value
* - As an optimization, ONLY trigger if value has actually changed. Could be deferred until later?
*/
$observe(path, callback) {
let fullPath = qualifiedEventName(this, path);
let eventOff =
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
let callbacksForPath = this._callbacksForPaths[path];
if (callbacksForPath === undefined) {
callbacksForPath = [];
this._callbacksForPaths[path] = callbacksForPath;
}
this._globalEventEmitter.on(fullPath, callback);
this._observers.push(eventOff);
callbacksForPath.push(callback);
return function unlisten() {
let index = callbacksForPath.indexOf(callback);
callbacksForPath.splice(index, 1);
if (callbacksForPath.length === 0) {
delete this._callbacksForPaths[path];
}
}.bind(this);
return eventOff;
}
$set(path, value) {
_.set(this, path, value);
@@ -88,25 +104,14 @@ class MutableDomainObject {
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.
}
$refresh(model) {
//TODO: Currently we are updating the entire object.
// In the future we could update a specific property of the object using the 'path' parameter.
const clone = JSON.parse(JSON.stringify(this));
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
//Emit wildcard event
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, '*', this, clone);
}
$on(event, callback) {
@@ -114,23 +119,53 @@ class MutableDomainObject {
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
while (this._observers.length > 0) {
const observer = this._observers.pop();
observer();
}
$updateListenersOnPath(updatedModel, mutatedPath, newValue, oldModel) {
const isRefresh = mutatedPath === '*';
Object.entries(this._callbacksForPaths).forEach(([observedPath, callbacks]) => {
if (isChildOf(observedPath, mutatedPath)
|| isParentOf(observedPath, mutatedPath)) {
let newValueOfObservedPath;
if (observedPath === '*') {
newValueOfObservedPath = updatedModel;
} else {
newValueOfObservedPath = _.get(updatedModel, observedPath);
}
if (isRefresh && observedPath !== '*') {
const oldValueOfObservedPath = _.get(oldModel, observedPath);
if (!_.isEqual(newValueOfObservedPath, oldValueOfObservedPath)) {
callbacks.forEach(callback => callback(newValueOfObservedPath));
}
} else {
//Assumed to be different if result of mutation.
callbacks.forEach(callback => callback(newValueOfObservedPath));
}
}
});
}
$synchronizeModel(updatedObject) {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(this, clone);
}
$destroy() {
Object.keys(this._callbacksForPaths).forEach(key => delete this._callbacksForPaths[key]);
this._instanceEventEmitter.emit('$_destroy');
this._globalEventEmitter.off(qualifiedEventName(this, '$_synchronize_model'), this.$synchronizeModel);
this._globalEventEmitter.off(qualifiedEventName(this, '*'), this.$updateListenersOnPath);
}
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));
utils.refresh(mutable, clone);
});
mutable.$updateListenersOnPath = mutable.$updateListenersOnPath.bind(mutable);
mutable.$synchronizeModel = mutable.$synchronizeModel.bind(mutable);
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '$_synchronize_model'), mutable.$synchronizeModel);
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '*'), mutable.$updateListenersOnPath);
return mutable;
}
@@ -147,4 +182,12 @@ function qualifiedEventName(object, eventName) {
return [keystring, eventName].join(':');
}
function isChildOf(observedPath, mutatedPath) {
return Boolean(mutatedPath === '*' || observedPath?.startsWith(mutatedPath));
}
function isParentOf(observedPath, mutatedPath) {
return Boolean(observedPath === '*' || mutatedPath?.startsWith(observedPath));
}
export default MutableDomainObject;

View File

@@ -1,7 +1,7 @@
import ObjectAPI from './ObjectAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Object API", () => {
fdescribe("The Object API", () => {
let objectAPI;
let typeRegistry;
let openmct = {};
@@ -287,53 +287,167 @@ describe("The Object API", () => {
mutableSecondInstance.$destroy();
});
it('to stay synchronized when mutated', function () {
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
});
it('to indicate when a property changes', function () {
let mutationCallback = jasmine.createSpy('mutation-callback');
let unlisten;
return new Promise(function (resolve) {
mutationCallback.and.callFake(resolve);
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
}).then(function () {
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
unlisten();
describe('on mutation', () => {
it('to stay synchronized', function () {
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
});
});
it('to indicate when a child property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
it('to indicate when a property changes', function () {
let mutationCallback = jasmine.createSpy('mutation-callback');
let unlisten;
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(embeddedObjectCallback).toHaveBeenCalledWith({
embeddedKey: 'updated-embedded-value'
return new Promise(function (resolve) {
mutationCallback.and.callFake(resolve);
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
}).then(function () {
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
unlisten();
});
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: {
});
it('to indicate when a child property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(embeddedObjectCallback).toHaveBeenCalledWith({
embeddedKey: 'updated-embedded-value'
}
});
});
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: {
embeddedKey: 'updated-embedded-value'
}
});
listeners.forEach(listener => listener());
listeners.forEach(listener => listener());
});
});
it('to indicate when a parent property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject', 'updated-embedded-value');
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: 'updated-embedded-value'
});
listeners.forEach(listener => listener());
});
});
});
describe('on refresh', () => {
let refreshModel;
beforeEach(() => {
refreshModel = JSON.parse(JSON.stringify(mutable));
});
it('to stay synchronized', function () {
refreshModel.otherAttribute = 'new-attribute-value';
mutable.$refresh(refreshModel);
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
});
it('to indicate when a property changes', function () {
let mutationCallback = jasmine.createSpy('mutation-callback');
let unlisten;
return new Promise(function (resolve) {
mutationCallback.and.callFake(resolve);
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
refreshModel.otherAttribute = 'some-new-value';
mutable.$refresh(refreshModel);
}).then(function () {
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
unlisten();
});
});
it('to indicate when a child property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
refreshModel.objectAttribute.embeddedObject.embeddedKey = 'updated-embedded-value';
mutable.$refresh(refreshModel);
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(embeddedObjectCallback).toHaveBeenCalledWith({
embeddedKey: 'updated-embedded-value'
});
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: {
embeddedKey: 'updated-embedded-value'
}
});
listeners.forEach(listener => listener());
});
});
it('to indicate when a parent property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
refreshModel.objectAttribute.embeddedObject = 'updated-embedded-value';
mutable.$refresh(refreshModel);
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: 'updated-embedded-value'
});
listeners.forEach(listener => listener());
});
});
});
});
});

View File

@@ -296,12 +296,17 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
this.unlistenToEntryChanges = this.openmct.objects.observe(this.domainObject, "configuration.entries", () => this.filterAndSortEntries());
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
if (this.unlistenToEntryChanges) {
this.unlistenToEntryChanges();
}
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
},

View File

@@ -233,6 +233,13 @@ export default {
},
mounted() {
this.dropOnEntry = this.dropOnEntry.bind(this);
this.$on('tags-updated', async () => {
const user = await this.openmct.user.getCurrentUser();
this.entry.modified = Date.now();
this.entry.modifiedBy = user.getId();
this.$emit('updateEntry', this.entry);
});
},
methods: {
async addNewEmbed(objectPath) {

View File

@@ -133,8 +133,11 @@ export default {
this.addedTags.push(newTagValue);
this.userAddingTag = true;
},
tagRemoved(tagToRemove) {
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
async tagRemoved(tagToRemove) {
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
this.$emit('tags-updated');
return result;
},
async tagAdded(newTag) {
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
@@ -146,6 +149,7 @@ export default {
this.tagsChanged(this.annotation.tags);
this.userAddingTag = false;
this.$emit('tags-updated');
}
}
};