2019-03-17 23:45:48 -05:00
|
|
|
const debug = require('debug')('@avocado/entity:traits');
|
|
|
|
import without from 'lodash.without';
|
|
|
|
import * as I from 'immutable';
|
|
|
|
|
|
|
|
import {Resource} from '@avocado/resource';
|
|
|
|
|
|
|
|
import {hasTrait, lookupTrait, registerTrait} from './registry';
|
|
|
|
|
|
|
|
function enumerateProperties(prototype) {
|
|
|
|
const result = {};
|
|
|
|
do {
|
|
|
|
Object.getOwnPropertyNames(prototype).forEach((property) => {
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
|
|
|
|
if (typeof descriptor.get === 'function') {
|
|
|
|
result[property] = result[property] || {};
|
|
|
|
result[property].get = true;
|
|
|
|
}
|
|
|
|
if (typeof descriptor.set === 'function') {
|
|
|
|
result[property] = result[property] || {};
|
|
|
|
result[property].set = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Traits {
|
|
|
|
|
|
|
|
constructor(entity) {
|
|
|
|
this.actions_PRIVATE = {};
|
|
|
|
this.entity_PRIVATE = entity;
|
|
|
|
this.hooks_PRIVATE = {};
|
|
|
|
this.properties_PRIVATE = {};
|
|
|
|
this.state_PRIVATE = I.Map();
|
|
|
|
this.traits_PRIVATE = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
acceptStateChange(change) {
|
|
|
|
for (const type in change) {
|
|
|
|
let instance = this.traits_PRIVATE[type];
|
|
|
|
// New trait requested?
|
|
|
|
if (!this.traits_PRIVATE[type]) {
|
|
|
|
// Doesn't exist?
|
|
|
|
if (!hasTrait(type)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this.addTrait(type, change[type]);
|
|
|
|
instance = this.traits_PRIVATE[type];
|
|
|
|
}
|
2019-03-18 21:22:54 -05:00
|
|
|
else {
|
|
|
|
// Accept state.
|
|
|
|
instance.acceptStateChange(change[type]);
|
|
|
|
}
|
|
|
|
this._setInstanceState(type, instance);
|
2019-03-17 23:45:48 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
allInstances() {
|
|
|
|
return this.traits_PRIVATE;
|
|
|
|
}
|
|
|
|
|
|
|
|
allTypes() {
|
|
|
|
return Object.keys(this.traits_PRIVATE);
|
|
|
|
}
|
|
|
|
|
|
|
|
addTrait(type, json) {
|
|
|
|
if (this.hasTrait(type)) {
|
|
|
|
debug(`Tried to add trait "${type}" when it already exists!`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!hasTrait(type)) {
|
|
|
|
debug(`Tried to add trait "${type}" which isn't registered!`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const Trait = lookupTrait(type);
|
|
|
|
// Ensure dependencies.
|
|
|
|
const dependencies = Trait.dependencies();
|
|
|
|
const allTypes = this.allTypes();
|
|
|
|
const lacking = without(dependencies, ...allTypes);
|
|
|
|
if (lacking.length > 0) {
|
|
|
|
debug(
|
|
|
|
`Tried to add trait "${type}" but lack one or more dependents: "${
|
|
|
|
lacking.join('", "')
|
|
|
|
}"!`
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Instantiate.
|
|
|
|
const instance = (new Trait(this.entity_PRIVATE)).fromJSON(json);
|
2019-03-18 20:00:02 -05:00
|
|
|
// Attach listeners.
|
|
|
|
const listeners = instance.listeners();
|
|
|
|
for (const eventName in listeners) {
|
|
|
|
this.entity_PRIVATE.on(
|
|
|
|
`${eventName}.trait-${type}`,
|
|
|
|
listeners[eventName]
|
|
|
|
);
|
|
|
|
}
|
2019-03-17 23:45:48 -05:00
|
|
|
// Proxy actions.
|
|
|
|
const actions = instance.actions();
|
|
|
|
for (const key in actions) {
|
|
|
|
this.actions_PRIVATE[key] = actions[key];
|
|
|
|
}
|
|
|
|
// Register hook listeners.
|
|
|
|
const hooks = instance.hooks();
|
|
|
|
for (const key in hooks) {
|
|
|
|
this.hooks_PRIVATE[key] = this.hooks_PRIVATE[key] || [];
|
|
|
|
this.hooks_PRIVATE[key].push({
|
|
|
|
fn: hooks[key],
|
|
|
|
type: Trait.type(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// Proxy properties.
|
|
|
|
const properties = enumerateProperties(Trait.prototype);
|
|
|
|
for (const key in properties) {
|
|
|
|
properties[key].instance = instance;
|
|
|
|
this.properties_PRIVATE[key] = properties[key];
|
|
|
|
}
|
|
|
|
// Add state.
|
2019-03-18 21:22:54 -05:00
|
|
|
this._setInstanceState(type, instance);
|
2019-03-17 23:45:48 -05:00
|
|
|
// Track trait.
|
|
|
|
this.traits_PRIVATE[type] = instance;
|
2019-03-18 20:05:41 -05:00
|
|
|
this.entity_PRIVATE.emit('traitAdded', instance);
|
2019-03-17 23:45:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
addTraits(traits) {
|
|
|
|
const allTypes = this.allTypes();
|
|
|
|
for (const type in traits) {
|
|
|
|
this.addTrait(type, traits[type]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fromJSON(json) {
|
|
|
|
this.addTraits(json);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
getProperty(property) {
|
|
|
|
if (property in this.actions_PRIVATE) {
|
|
|
|
return this.actions_PRIVATE[property];
|
|
|
|
}
|
|
|
|
if (property in this.properties_PRIVATE) {
|
|
|
|
const instance = this.properties_PRIVATE[property].instance;
|
|
|
|
if (!this.properties_PRIVATE[property].get) {
|
|
|
|
const type = instance.constructor.type();
|
|
|
|
throw new ReferenceError(
|
|
|
|
`Property '${property}' from Trait '${type}' has no getter`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return instance[property];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hasProperty(property) {
|
|
|
|
if (property in this.actions_PRIVATE) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (property in this.properties_PRIVATE) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
hasTrait(type) {
|
|
|
|
return type in this.traits_PRIVATE;
|
|
|
|
}
|
|
|
|
|
|
|
|
hydrate() {
|
|
|
|
const promises = [];
|
|
|
|
for (const type in this.traits_PRIVATE) {
|
|
|
|
const instance = this.traits_PRIVATE[type];
|
|
|
|
promises.push(instance.hydrate());
|
|
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
|
|
|
instance(type) {
|
|
|
|
return this.traits_PRIVATE[type];
|
|
|
|
}
|
|
|
|
|
|
|
|
invokeHook(hook, ...args) {
|
|
|
|
const results = {};
|
|
|
|
if (!(hook in this.hooks_PRIVATE)) {
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
for (const {fn, type} of this.hooks_PRIVATE[hook]) {
|
|
|
|
results[type] = fn(...args);
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
invokeHookFlat(hook, ...args) {
|
|
|
|
const invokeResults = this.invokeHook(hook, ...args);
|
|
|
|
const results = [];
|
|
|
|
for (const type in invokeResults) {
|
|
|
|
results.push(invokeResults[type]);
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
removeAllTraits() {
|
|
|
|
const types = this.allTypes();
|
|
|
|
this.removeTraits(types);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTrait(type) {
|
|
|
|
if (!this.hasTrait(type)) {
|
|
|
|
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const instance = this.traits_PRIVATE[type];
|
|
|
|
|
|
|
|
const actions = instance.actions();
|
|
|
|
for (const key in actions) {
|
|
|
|
delete this.actions_PRIVATE[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
const hooks = instance.hooks();
|
|
|
|
for (const key in hooks) {
|
|
|
|
delete this.hooks_PRIVATE[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
const Trait = lookupTrait(type);
|
|
|
|
const properties = enumerateProperties(Trait.prototype);
|
|
|
|
for (const key in properties) {
|
|
|
|
delete this.properties_PRIVATE[key];
|
|
|
|
}
|
|
|
|
|
2019-03-18 20:00:02 -05:00
|
|
|
this.entity_PRIVATE.off(`.trait-${type}`);
|
|
|
|
|
2019-03-17 23:45:48 -05:00
|
|
|
instance.destroy();
|
|
|
|
|
|
|
|
this.state_PRIVATE = this.state_PRIVATE.delete(type);
|
|
|
|
delete this.traits_PRIVATE[type];
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTraits(types) {
|
|
|
|
types.forEach((type) => this.removeTrait(type));
|
|
|
|
}
|
|
|
|
|
2019-03-18 21:22:54 -05:00
|
|
|
_setInstanceState(type, instance) {
|
|
|
|
const state = this.state_PRIVATE;
|
|
|
|
let map = state.has(type) ? state.get(type) : I.Map();
|
|
|
|
map = map.set('state', instance.state);
|
|
|
|
map = map.set('params', instance.params);
|
|
|
|
this.state_PRIVATE = this.state_PRIVATE.set(type, map);
|
|
|
|
}
|
|
|
|
|
2019-03-17 23:45:48 -05:00
|
|
|
setProperty(property, value, receiver) {
|
|
|
|
if (property in this.properties_PRIVATE) {
|
|
|
|
const instance = this.properties_PRIVATE[property].instance;
|
|
|
|
const type = instance.constructor.type();
|
|
|
|
if (!this.properties_PRIVATE[property].set) {
|
|
|
|
throw new ReferenceError(
|
|
|
|
`Property '${property}' from Trait '${type}' has no setter`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
instance[property] = value;
|
2019-03-18 21:22:54 -05:00
|
|
|
this._setInstanceState(type, instance);
|
2019-03-17 23:45:48 -05:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
state() {
|
|
|
|
return this.state_PRIVATE;
|
|
|
|
}
|
|
|
|
|
|
|
|
toJSON() {
|
|
|
|
const json = {};
|
|
|
|
for (const type in this.traits_PRIVATE) {
|
|
|
|
json[type] = this.traits_PRIVATE[type].toJSON();
|
|
|
|
}
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|