UNPKG

@ayanaware/bento

Version:

Modular runtime framework designed to solve complex tasks

698 lines 25.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EntityManager = exports.PluginHook = void 0; const errors_1 = require("@ayanaware/errors"); const ChildOf_1 = require("../decorators/ChildOf"); const Inject_1 = require("../decorators/Inject"); const Parent_1 = require("../decorators/Parent"); const Subscribe_1 = require("../decorators/Subscribe"); const Variable_1 = require("../decorators/Variable"); const EntityError_1 = require("../errors/EntityError"); const EntityRegistrationError_1 = require("../errors/EntityRegistrationError"); const isClass_1 = require("../util/isClass"); const EntityEvents_1 = require("./EntityEvents"); const ReferenceManager_1 = require("./ReferenceManager"); const ComponentAPI_1 = require("./api/ComponentAPI"); const PluginAPI_1 = require("./api/PluginAPI"); const Entity_1 = require("./interfaces/Entity"); var PluginHook; (function (PluginHook) { PluginHook["PRE_COMPONENT_LOAD"] = "onPreComponentLoad"; PluginHook["PRE_COMPONENT_UNLOAD"] = "onPreComponentUnload"; PluginHook["POST_COMPONENT_LOAD"] = "onPostComponentLoad"; PluginHook["POST_COMPONENT_UNLOAD"] = "onPostComponentUnload"; })(PluginHook = exports.PluginHook || (exports.PluginHook = {})); class EntityManager { constructor(bento) { this.events = new Map(); this.entities = new Map(); this.pending = new Map(); this.references = new ReferenceManager_1.ReferenceManager(); this.bento = bento; } resolveReference(reference, error) { return this.references.resolve(reference, error); } /** * Check if EntityEvents exists * @param reference EntityReference * * @returns boolean */ hasEvents(reference) { const name = this.references.resolve(reference); return this.events.has(name); } /** * Get EntityEvents or create it * @param reference EntityReference * * @returns EntityEvents */ getEvents(reference) { const name = this.references.resolve(reference, true); if (!this.hasEvents(name)) { const events = new EntityEvents_1.EntityEvents(name, this.bento.options); this.events.set(name, events); return events; } return this.events.get(name); } /** * Get loaded entities * @param type EntityType * * @returns Entity Map */ getEntities(type) { const entities = Array.from(this.entities.entries()) .filter(([, entity]) => !type || entity.type === type); return new Map(entities); } /** * Check if Entity exists * @param reference EntityReference * * @returns boolean */ hasEntity(reference) { const name = this.references.resolve(reference); return this.getEntities().has(name); } /** * Get Entity * @param reference EntityReference * * @returns Entity or null */ getEntity(reference) { const name = this.references.resolve(reference); return (this.getEntities().get(name) || null); } /** * Add a Entity to Bento * @param entity - Entity * * @returns Entity name */ async addEntity(entity) { if (typeof entity === 'function' && (0, isClass_1.isClass)(entity)) entity = new entity(); if (entity == null || typeof entity !== 'object') throw new errors_1.IllegalArgumentError('Entity must be a object'); if (typeof entity.name !== 'string') throw new errors_1.IllegalArgumentError('Entity name must be a string'); if (!entity.name) throw new EntityRegistrationError_1.EntityRegistrationError(entity, 'Entity must specify a name'); if (typeof entity.type !== 'string') throw new errors_1.IllegalArgumentError('Entity type must be a string'); if (!entity.type) throw new EntityRegistrationError_1.EntityRegistrationError(entity, 'Entity type must be specificed'); if (this.hasEntity(entity.name)) { const oldEntity = this.getEntity(entity.name); if (!oldEntity.replaceable) throw new errors_1.IllegalArgumentError(`Entity name "${entity.name}" is already in use, and is not replaceable`); return this.replaceEntity(oldEntity, entity); } this.prepareEntity(entity); return this.loadEntity(entity); } /** * Replace entity and rewrite references and name behind the scenes. * * This allows for the following code to work: * ```ts * class Old { name = 'old'; replaceable = true } * class New { name = 'new' } * * await bento.replaceEntity(Old, new New()); * * bento.getEntity(Old); // Would actually return Instance of the New class * ``` * * @param reference EntityReference * @param entity Entity */ async replaceEntity(reference, entity) { const oldEntity = this.getEntity(reference); if (!oldEntity) throw new errors_1.IllegalArgumentError('Entity to replace does not exist'); // Handle uninstantiated Entity if (typeof entity === 'function' && (0, isClass_1.isClass)(entity)) entity = new entity(); if (entity == null || typeof entity !== 'object') throw new errors_1.IllegalArgumentError('Entity must be a object'); if (typeof entity.name !== 'string') throw new errors_1.IllegalArgumentError('Entity name must be a string'); if (!entity.name) throw new EntityRegistrationError_1.EntityRegistrationError(entity, 'Entity must specify a name'); if (typeof entity.type !== 'string') throw new errors_1.IllegalArgumentError('Entity type must be a string'); if (!entity.type) throw new EntityRegistrationError_1.EntityRegistrationError(entity, 'Entity type must be specificed'); if (!oldEntity.replaceable) throw new errors_1.IllegalArgumentError(`Entity name "${entity.name}" is not marked as replaceable`); this.prepareEntity(entity); // Remove old Entity from Bento let removed = await this.removeEntity(oldEntity.name); removed = removed.filter(e => e.name !== oldEntity.name); // Continue to track constructor // Allowing Bento to still resolve the name of old entities this.references.add(oldEntity, entity.name); // Rewrite if needed, for string users if (oldEntity.name !== entity.name) { this.references.addRewrite(oldEntity, entity.name); } // Load the new Entity const name = await this.loadEntity(entity); // Restore Dependents for (const item of removed) await this.addEntity(item); return name; } /** * Remove a Entity from Bento * @param reference - Name of Entity */ async removeEntity(reference) { const name = this.references.resolve(reference); const entity = this.getEntity(name); if (!entity) throw new errors_1.IllegalStateError(`Entity "${name}" is not currently loaded.`); let removed = []; // if we have any dependents lets unload them first const dependents = this.getEntityDependents(entity); for (const dependent of dependents) { // skip already removed entities if (removed.find(d => d.name === dependent.name) != null) continue; const also = await this.removeEntity(dependent.name); removed = [...removed, ...also]; } // onPreComponentUnload if (entity.type === Entity_1.EntityType.COMPONENT) { await this.handlePluginHook(PluginHook.PRE_COMPONENT_UNLOAD, entity); } // call onUnload if (typeof entity.onUnload === 'function') { try { await entity.onUnload(); } catch { // Ignore } } removed.push(entity); // if we were a child, inform parent of our unloading if (entity.parent) { entity.parent = this.references.resolve(entity.parent); if (this.hasEntity(entity.parent)) { const parent = this.getEntity(entity.parent); if (typeof parent.onChildUnload === 'function') { try { await parent.onChildUnload(entity); } catch (e) { // Ignore } } } } // remove all event subscriptions entity.api.unsubscribeAll(); // remove reference this.references.remove(entity); // delete entity if (this.hasEntity(entity.name)) { this.entities.delete(entity.name); } // onPostComponentUnload if (entity.type === Entity_1.EntityType.COMPONENT) { await this.handlePluginHook(PluginHook.POST_COMPONENT_UNLOAD, entity); } return removed; } /** * Check if Plugin exists * @param reference PluginReference * * @returns boolean */ hasPlugin(reference) { const name = this.references.resolve(reference); return this.getEntities(Entity_1.EntityType.PLUGIN).has(name); } /** * Get all Plugins * * @returns Plugin Map */ getPlugins() { return this.getEntities(Entity_1.EntityType.PLUGIN); } /** * Get Plugin * @param reference PluginReference * * @returns Plugin or null */ getPlugin(reference) { const name = this.references.resolve(reference); return (this.getEntities(Entity_1.EntityType.PLUGIN).get(name) || null); } /** * Adds plugins in order of array * @param plugins - array of plugins * * @returns Array of loaded plugin names */ async addPlugins(plugins) { if (!Array.isArray(plugins)) throw new errors_1.IllegalArgumentError('addPlugins only accepts an array.'); const results = []; for (const plugin of plugins) { const name = await this.addPlugin(plugin); results.push(name); } return results; } async addPlugin(plugin) { if (typeof plugin === 'function' && (0, isClass_1.isClass)(plugin)) plugin = new plugin(); Object.defineProperty(plugin, 'type', { configurable: true, writable: false, enumerable: true, value: Entity_1.EntityType.PLUGIN, }); return this.addEntity(plugin); } async replacePlugin(reference, plugin) { if (typeof plugin === 'function' && (0, isClass_1.isClass)(plugin)) plugin = new plugin(); Object.defineProperty(plugin, 'type', { configurable: true, writable: false, enumerable: true, value: Entity_1.EntityType.PLUGIN, }); return this.replaceEntity(reference, plugin); } async removePlugin(plugin) { return this.removeEntity(plugin); } /** * Check if Component exists * @param reference ComponentReference * * @returns boolean */ hasComponent(reference) { const name = this.references.resolve(reference); return this.getEntities(Entity_1.EntityType.COMPONENT).has(name); } /** * Get all Components * * @returns Component Map */ getComponents() { return this.getEntities(Entity_1.EntityType.COMPONENT); } /** * Adds components in order of array * @param components - array of plugins * * @returns Array of loaded plugin names */ async addComponents(components) { if (!Array.isArray(components)) throw new errors_1.IllegalArgumentError('addComponents only accepts an array'); const results = []; for (const component of components) { const name = await this.addComponent(component); results.push(name); } return results; } /** * Get Component * @param reference ComponentReference * * @returns Component or null */ getComponent(reference) { const name = this.references.resolve(reference); return (this.getEntities(Entity_1.EntityType.COMPONENT).get(name) || null); } /** * Add Component * @param component Component * * @returns Component Name */ async addComponent(component) { if (typeof component === 'function' && (0, isClass_1.isClass)(component)) component = new component(); Object.defineProperty(component, 'type', { configurable: true, writable: false, enumerable: true, value: Entity_1.EntityType.COMPONENT, }); return this.addEntity(component); } /** * Replace Component * @param reference ComponentReference * @param component Component * * @returns Component Name */ async replaceComponent(reference, component) { if (typeof component === 'function' && (0, isClass_1.isClass)(component)) component = new component(); Object.defineProperty(component, 'type', { configurable: true, writable: false, enumerable: true, value: Entity_1.EntityType.COMPONENT, }); return this.replaceEntity(reference, component); } /** * Remove Component * @param reference ComponentReference * * @returns Removed Entities */ async removeComponent(reference) { return this.removeEntity(reference); } /** * Verify loaded entites & call their onVerify() hook * @returns Map of string/Entity pairs */ async verify() { const pending = this.getPendingEntities(); if (pending.length > 0) { throw new errors_1.IllegalStateError(`One or more entities are still in a pending state: '${pending.map(p => p.name).join('\', \'')}'`); } const entities = this.getEntities(); for (const [name, entity] of entities.entries()) { if (typeof entity.onVerify !== 'function') continue; try { await entity.onVerify(); } catch (e) { throw new EntityError_1.EntityError(`Entity "${name}".onVerify() threw error`).setCause(e); } } return entities; } /** * @see PendingEntityInfo * @returns - All currently pending bento entities and their info */ getPendingEntities() { const pending = []; for (const [name, entity] of this.pending.entries()) { // get pending items const missing = this.getMissingDependencies(entity); pending.push({ name, entity, missing, }); } return pending; } /** * Get all dependents of a entity * @param entity - EntityReference * * @returns Array of Entities */ getEntityDependents(entity) { const name = this.references.resolve(entity); if (!this.hasEntity(name)) throw new errors_1.IllegalStateError(`Entity "${name}" does not exist`); const dependents = []; for (const item of this.getEntities().values()) { for (const dependency of item.dependencies) { if (name === this.references.resolve(dependency)) { dependents.push(item); } else if (item.parent && name === this.references.resolve(item.parent)) { dependents.push(item); } } } return dependents; } /** * Get missing depenencies of an Entity * @param reference EntityReference * * @returns EntityReference Array */ getMissingDependencies(entityOrReference) { let entity; if (this.hasEntity(entityOrReference)) entity = this.getEntity(entityOrReference); else entity = entityOrReference; // by this point we should have a entity instance, verify if (entity == null || typeof entity !== 'object') throw new errors_1.IllegalArgumentError('Entity must be an object'); if (entity.dependencies == null || !Array.isArray(entity.dependencies)) { throw new errors_1.IllegalArgumentError('Entity dependencies must be an array'); } const dependencies = []; for (const dependency of entity.dependencies) { const name = this.references.resolve(dependency); // name failed to resolve or entity is not loaded if (!name || !this.hasEntity(name)) dependencies.push(dependency); } return dependencies; } /** * Ensures all entity dependencies resolve to a name, and no duplicates * @param entity Entity */ resolveDependencies(entity) { const dependencies = []; for (const dependency of entity.dependencies) { const name = this.references.resolve(dependency); if (!name) { dependencies.push(dependency); continue; } if (name === entity.name) continue; else if (!dependencies.includes(name)) dependencies.push(name); } entity.dependencies = dependencies; } /** * Handle pending entities * * @returns Promise */ async handlePendingEntities() { let loaded = 0; for (const entity of this.pending.values()) { // convert depdendencies again, for any that were not loaded previously this.resolveDependencies(entity); const missing = this.getMissingDependencies(entity); if (missing.length > 0) continue; this.pending.delete(entity.name); try { await this.handleLifecycle(entity); loaded++; } catch (e) { this.pending.set(entity.name, entity); throw e; } } if (loaded > 0) return this.handlePendingEntities(); } /** * Prepare Decorators * @param entity Entity */ prepareDecorators(entity) { // @Inject (0, Inject_1.getInjections)(entity.constructor).forEach(i => entity.dependencies.push(i.reference)); // @Subscribe (0, Subscribe_1.getSubscriptions)(entity.constructor).forEach(s => entity.dependencies.push(s.reference)); // @ChildOf const childOf = (0, ChildOf_1.getChildOf)(entity.constructor); if (childOf !== null) entity.parent = childOf; // @Parent const parent = (0, Parent_1.getParent)(entity.constructor); if (parent !== null) entity.parent = parent.reference; } /** * Handle Decorators * @param entity Entity */ handleDecorators(entity) { // @Inject Decorator (0, Inject_1.getInjections)(entity.constructor).forEach(i => entity.api.injectEntity(i.reference, i.key)); // @Subscribe Decorator (0, Subscribe_1.getSubscriptions)(entity.constructor).forEach(s => entity.api.subscribe(s.reference, s.event, s.handler, entity)); // @Variable Decorator (0, Variable_1.getVariables)(entity.constructor).forEach(v => entity.api.injectVariable(v.definition, v.key)); // @Parent Decorator const parent = (0, Parent_1.getParent)(entity.constructor); if (parent !== null) entity.api.injectEntity(parent.reference, parent.propertyKey); } /** * Enforces Bento API and prepares entity for loading * @param entity - Entity */ prepareEntity(entity) { // take control and redefine entity name Object.defineProperty(entity, 'name', { configurable: true, writable: false, enumerable: true, value: entity.name, }); // Check dependencies if (entity.dependencies == null) entity.dependencies = []; if (entity.dependencies != null && !Array.isArray(entity.dependencies)) { throw new EntityRegistrationError_1.EntityRegistrationError(entity, `${entity.name}.dependencies must be an array`); } if (entity.constructor) { // if entity has constructor track it this.references.add(entity); // prepare decorators this.prepareDecorators(entity); } // Append parent to dependencies if (entity.parent) entity.dependencies.push(entity.parent); // convert dependencies this.resolveDependencies(entity); } /** * Load Entity into Bento or deffer for dependencies * @param entity Entity * * @returns Entity Name */ async loadEntity(entity) { // Create API instance let api; if (entity.type === Entity_1.EntityType.PLUGIN) api = new PluginAPI_1.PluginAPI(this.bento, entity); else if (entity.type === Entity_1.EntityType.COMPONENT) api = new ComponentAPI_1.ComponentAPI(this.bento, entity); Object.defineProperty(entity, 'api', { configurable: true, writable: false, enumerable: true, value: api, }); // determine dependencies const missing = this.getMissingDependencies(entity); if (missing.length === 0) { // All dependencies are already loaded, handle lifecycle events await this.handleLifecycle(entity); // if any pending entities, attempt to handle them now if (this.pending.size > 0) await this.handlePendingEntities(); } else { // not able to handle lifecycle yet :c this.pending.set(entity.name, entity); } return entity.name; } /** * Handle Entity Lifecycle * @param entity Entity */ async handleLifecycle(entity) { // handle parent let parent = null; if (entity.parent) { entity.parent = this.references.resolve(entity.parent); if (!this.hasEntity(entity.parent)) throw new errors_1.IllegalStateError(`Child entity "${entity.name}" loaded prior to Parent entity "${entity.parent}"`); // aka, universe bork parent = this.getEntity(entity.parent); } // No class? No Decorators! hmpf! if (entity.constructor) this.handleDecorators(entity); // if we are a plugin verify we are not depending on a component if (entity.type === Entity_1.EntityType.PLUGIN) { for (const reference of entity.dependencies) { const dependency = this.getEntity(reference); if (dependency.type === Entity_1.EntityType.COMPONENT) throw new EntityRegistrationError_1.EntityRegistrationError(entity, `Cannot depend on Component "${dependency.name}"`); } } // onPreComponentLoad if (entity.type === Entity_1.EntityType.COMPONENT) { await this.handlePluginHook(PluginHook.PRE_COMPONENT_LOAD, entity); } // dont move this: plugins must be defined before calling onload. // this is because plugins can add components and other objects // that will then need the plugin itself to continue loading this.entities.set(entity.name, entity); // Call onLoad if present if (typeof entity.onLoad === 'function') { try { await entity.onLoad(entity.api); } catch (e) { throw new EntityRegistrationError_1.EntityRegistrationError(entity, `${entity.name}.onLoad() threw error`).setCause(e); } } // if we just loaded a child entity, lets inform the parent if (parent != null && typeof parent.onChildLoad === 'function') { try { await parent.onChildLoad(entity); } catch (e) { throw new EntityRegistrationError_1.EntityRegistrationError(parent, `Failed to load child entity "${entity.name}"`).setCause(e); } } // onPostComponentLoad if (entity.type === Entity_1.EntityType.COMPONENT) { await this.handlePluginHook(PluginHook.POST_COMPONENT_LOAD, entity); } } /** * Call Hook for all plugins * @param hookName Hook name * @param component Component */ async handlePluginHook(hookName, component) { for (const plugin of this.getEntities(Entity_1.EntityType.PLUGIN).values()) { if (typeof plugin[hookName] !== 'function') continue; try { await plugin[hookName](component); } catch (e) { throw new errors_1.ProcessingError(`Plugin "${plugin.name}" ${hookName} hook threw an error`).setCause(e); } } } } exports.EntityManager = EntityManager; //# sourceMappingURL=EntityManager.js.map