Compare commits

...

1 Commits

Author SHA1 Message Date
d16c60aa7a Initial pass at refactoring composition. Completely broken 2023-01-25 18:23:05 -08:00
3 changed files with 59 additions and 92 deletions

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter';
/** /**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject * @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/ */
@ -60,6 +61,10 @@ export default class CompositionCollection {
#publicAPI; #publicAPI;
#listeners; #listeners;
#mutables; #mutables;
#onGlobalAdd;
#onGlobalRemove;
static #globalEvents = new EventEmitter();
/** /**
* @constructor * @constructor
* @param {DomainObject} domainObject the domain object * @param {DomainObject} domainObject the domain object
@ -95,6 +100,21 @@ export default class CompositionCollection {
unobserve(); unobserve();
}); });
} }
const keyString = publicAPI.objects.makeKeyString(domainObject.identifier);
this.#onGlobalAdd = this._onGlobalAdd.bind(this);
this.#onGlobalRemove = this._onGlobalRemove.bind(this);
CompositionCollection.#globalEvents.on(`add:${keyString}`, this.#onGlobalAdd);
CompositionCollection.#globalEvents.on(`remove:${keyString}`, this.#onGlobalRemove);
}
_onGlobalAdd(object) {
this.#emit('add', object);
}
_onGlobalRemove(identifier) {
this.#emit('remove', identifier);
} }
/** /**
* Listen for changes to this composition. Supports 'add', 'remove', and * Listen for changes to this composition. Supports 'add', 'remove', and
@ -209,23 +229,21 @@ export default class CompositionCollection {
* **Intended for internal use ONLY.** * **Intended for internal use ONLY.**
* true if the underlying provider should not be updated. * true if the underlying provider should not be updated.
*/ */
add(child, skipMutate) { add(child) {
if (!skipMutate) { if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) { throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
this.#provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
this.#emit('add', child);
} }
this.#provider.add(this.domainObject, child.identifier);
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
// CompositionCollection.#globalEvents.emit(`add:${keyString}`, child);
} }
/** /**
* Load the domain objects in this composition. * Load the domain objects in this composition.
@ -240,7 +258,12 @@ export default class CompositionCollection {
this.#cleanUpMutables(); this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject); const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal))); const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
childObjects.forEach(c => this.add(c, true)); childObjects.forEach(c => {
this.add(c);
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
CompositionCollection.#globalEvents.emit(`add:${keyString}`, c);
});
this.#emit('load'); this.#emit('load');
return childObjects; return childObjects;
@ -259,20 +282,18 @@ export default class CompositionCollection {
* true if the underlying provider should not be updated. * true if the underlying provider should not be updated.
* @name remove * @name remove
*/ */
remove(child, skipMutate) { remove(child) {
if (!skipMutate) { this.#provider.remove(this.domainObject, child.identifier);
this.#provider.remove(this.domainObject, child.identifier); if (this.returnMutables) {
} else { let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.returnMutables) { if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
let keyString = this.#publicAPI.objects.makeKeyString(child); this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) { delete this.#mutables[keyString];
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
}
} }
this.#emit('remove', child);
} }
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
// CompositionCollection.#globalEvents.emit(`remove:${keyString}`, child.identifier);
} }
/** /**
* Reorder the domain objects in this composition. * Reorder the domain objects in this composition.
@ -295,6 +316,10 @@ export default class CompositionCollection {
this.mutationListener(); this.mutationListener();
delete this.mutationListener; delete this.mutationListener;
} }
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
CompositionCollection.#globalEvents.off(`add:${keyString}`, this.#onGlobalAdd);
CompositionCollection.#globalEvents.off(`remove:${keyString}`, this.#onGlobalRemove);
} }
/** /**
* Handle reorder from provider. * Handle reorder from provider.

View File

@ -71,10 +71,6 @@ export default class CompositionProvider {
return this.#listeningTo; return this.#listeningTo;
} }
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
get publicAPI() { get publicAPI() {
return this.#publicAPI; return this.#publicAPI;
} }
@ -181,22 +177,6 @@ export default class CompositionProvider {
throw new Error("This method must be implemented by a subclass."); throw new Error("This method must be implemented by a subclass.");
} }
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
}
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}
/** /**
* @private * @private
* @param {DomainObject} parent * @param {DomainObject} parent
@ -216,47 +196,5 @@ export default class CompositionProvider {
#supportsComposition(parent, _child) { #supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent); return this.#publicAPI.composition.supportsComposition(parent);
} }
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
} }

View File

@ -89,7 +89,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
event, event,
callback, callback,
context) { context) {
this.establishTopicListener(); //this.establishTopicListener();
/** @type {string} */ /** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier); const keyString = objectUtils.makeKeyString(domainObject.identifier);
@ -157,6 +157,8 @@ export default class DefaultCompositionProvider extends CompositionProvider {
}); });
this.publicAPI.objects.mutate(domainObject, 'composition', composition); this.publicAPI.objects.mutate(domainObject, 'composition', composition);
this.objectListeners.remove?.forEach(listener => listener.callback.apply(listener.context, childId));
} }
/** /**
* Add a domain object to another domain object's composition. * Add a domain object to another domain object's composition.
@ -174,6 +176,8 @@ export default class DefaultCompositionProvider extends CompositionProvider {
if (!this.includes(parent, childId)) { if (!this.includes(parent, childId)) {
parent.composition.push(childId); parent.composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', parent.composition); this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
this.objectListeners.add?.forEach(listener => listener.callback.apply(listener.context, childId));
} }
} }