tessl install tessl/npm-ecos@0.2.0Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern
Understanding the order of operations during entity creation is crucial for working effectively with ECOS. This guide provides a detailed explanation of when each operation occurs.
When you call a factory's create() method or a custom constructor, ECOS performs operations in a specific sequence. Understanding this sequence helps you avoid common pitfalls and use the system effectively.
When an entity is created, operations occur in this order:
type propertyidextend option are appliedprops array are initialized to nulldefault option are assignedcreate(options) are assigned (overriding defaults)Let's trace through entity creation step by step:
const { factory, extenders } = require('ecos');
// Register an extender
factory.registerExtender('logger', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Step 3-4: Extender runs');
console.log('Properties available:', entity);
}
});
// Register a preset with an extender
factory.registerPreset('withTimestamp', {
extend: ['timestamps']
});
factory.registerExtender('timestamps', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Step 3: Preset extender runs');
entity.createdAt = Date.now();
}
});
// Create a factory
const myFactory = factory.create({
name: 'example',
presets: ['withTimestamp'],
props: ['name', 'value'],
default: { name: 'default', value: 0 },
extend: ['logger']
});
console.log('Creating entity...');
const entity = myFactory.create({ value: 42 });
console.log('Entity created:', entity);Output:
Creating entity...
Step 3: Preset extender runs
Step 3-4: Extender runs
Properties available: { type: 'example', id: 0, createdAt: 1234567890 }
Entity created: {
type: 'example',
id: 0,
createdAt: 1234567890,
name: 'default',
value: 42
}Notice that in the extender, name and value are NOT yet available. They're added in steps 5-7, after extenders run.
Most important constraint: Extenders execute BEFORE properties are assigned.
const { factory, extenders } = require('ecos');
factory.registerExtender('computeStats', {
type: extenders.FUNCTION,
handler: function(entity) {
// ✗ WRONG: baseHealth is undefined here!
entity.maxHealth = entity.baseHealth * 2;
}
});
const characterFactory = factory.create({
name: 'character',
props: ['baseHealth'],
default: { baseHealth: 100 },
extend: ['computeStats']
});
const character = characterFactory.create();
console.log(character.maxHealth); // NaN (undefined * 2)When you need to access properties, use methods instead of extenders:
const { factory } = require('ecos');
factory.registerMethod('computeStats', function() {
// ✓ CORRECT: baseHealth is available when method is called
this.maxHealth = this.baseHealth * 2;
});
const characterFactory = factory.create({
name: 'character',
props: ['baseHealth'],
default: { baseHealth: 100 },
methods: ['computeStats']
});
const character = characterFactory.create({ baseHealth: 150 });
character.computeStats();
console.log(character.maxHealth); // 300Initialize with default values in extenders, then update based on properties:
const { factory, extenders } = require('ecos');
// Initialize with default values
factory.registerExtender('initStats', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.stats = {
health: 100, // Default value
mana: 50 // Default value
};
}
});
// Update based on properties
factory.registerMethod('finalizeStats', function() {
this.stats.health = this.baseHealth || 100;
this.stats.mana = this.baseMana || 50;
});
const characterFactory = factory.create({
name: 'character',
props: ['baseHealth', 'baseMana'],
default: { baseHealth: 150, baseMana: 75 },
extend: ['initStats'],
methods: ['finalizeStats']
});
const character = characterFactory.create();
character.finalizeStats();
console.log(character.stats); // { health: 150, mana: 75 }Extenders execute in the order they appear:
const { factory, extenders } = require('ecos');
factory.registerExtender('first', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('First extender');
entity.order = [];
entity.order.push('first');
}
});
factory.registerExtender('second', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Second extender');
entity.order.push('second');
}
});
factory.registerExtender('third', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Third extender');
entity.order.push('third');
}
});
const myFactory = factory.create({
name: 'ordered',
extend: ['first', 'second', 'third']
});
const entity = myFactory.create();
// Output:
// First extender
// Second extender
// Third extender
console.log(entity.order); // ['first', 'second', 'third']Extenders from presets execute before factory extenders:
const { factory, extenders } = require('ecos');
factory.registerExtender('presetExtender', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Preset extender');
entity.order = ['preset'];
}
});
factory.registerExtender('factoryExtender', {
type: extenders.FUNCTION,
handler: function(entity) {
console.log('Factory extender');
entity.order.push('factory');
}
});
factory.registerPreset('myPreset', {
extend: ['presetExtender']
});
const myFactory = factory.create({
name: 'example',
presets: ['myPreset'],
extend: ['factoryExtender']
});
const entity = myFactory.create();
// Output:
// Preset extender
// Factory extender
console.log(entity.order); // ['preset', 'factory']When using multiple presets, their extenders execute in preset order:
const { factory, extenders } = require('ecos');
factory.registerExtender('ext1', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.order = entity.order || [];
entity.order.push('ext1');
}
});
factory.registerExtender('ext2', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.order.push('ext2');
}
});
factory.registerExtender('ext3', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.order.push('ext3');
}
});
factory.registerPreset('preset1', { extend: ['ext1'] });
factory.registerPreset('preset2', { extend: ['ext2'] });
const myFactory = factory.create({
name: 'example',
presets: ['preset1', 'preset2'],
extend: ['ext3']
});
const entity = myFactory.create();
console.log(entity.order); // ['ext1', 'ext2', 'ext3']Properties from the props array are initialized to null:
const myFactory = factory.create({
name: 'example',
props: ['a', 'b', 'c']
});
const entity = myFactory.create();
console.log(entity); // { type: 'example', id: 0, a: null, b: null, c: null }Values from default override the null initialization:
const myFactory = factory.create({
name: 'example',
props: ['a', 'b', 'c'],
default: { a: 1, b: 2 }
});
const entity = myFactory.create();
console.log(entity); // { type: 'example', id: 0, a: 1, b: 2, c: null }Values from create(options) override defaults:
const myFactory = factory.create({
name: 'example',
props: ['a', 'b', 'c'],
default: { a: 1, b: 2, c: 3 }
});
const entity = myFactory.create({ b: 20, c: 30 });
console.log(entity); // { type: 'example', id: 0, a: 1, b: 20, c: 30 }Methods are attached last, after all properties are set:
const { factory } = require('ecos');
factory.registerMethod('logState', function() {
console.log('Value:', this.value);
});
const myFactory = factory.create({
name: 'example',
props: ['value'],
default: { value: 0 },
methods: ['logState']
});
const entity = myFactory.create({ value: 42 });
// Methods are available immediately after create() returns
entity.logState(); // 'Value: 42'Custom constructors follow the same flow, but pass predefined values to create():
const unitFactory = factory.create({
name: 'unit',
props: ['health', 'attack'],
default: { health: 100, attack: 10 },
custom: {
warrior: { health: 150, attack: 20 }
}
});
// This:
const warrior = unitFactory.warrior();
// Is equivalent to:
const warrior2 = unitFactory.create({ health: 150, attack: 20 }, 'warrior');
// Both follow the same execution order┌─────────────────────────────────────────────┐
│ factoryInstance.create(options, customName) │
└──────────────────┬──────────────────────────┘
│
▼
┌───────────────────────┐
│ 1. Create base object │
│ { type: '...' } │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 2. Assign ID │
│ entities.set(obj) │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 3. Apply preset │
│ extenders │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 4. Apply factory │
│ extenders │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 5. Initialize props │
│ (set to null) │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 6. Apply defaults │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 7. Apply create │
│ options │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 8. Attach methods │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Return entity │
└───────────────────────┘Use extenders to initialize structure (arrays, objects) and methods for logic that depends on properties:
// Extender: Initialize structure
factory.registerExtender('initInventory', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.inventory = [];
entity.maxSlots = 10; // Default
}
});
// Method: Logic based on properties
factory.registerMethod('setInventorySize', function() {
this.maxSlots = this.level * 5; // Uses entity property
});GETSET extenders define getters/setters that are called later, so they can safely access properties:
factory.registerExtender('computedProperty', {
type: extenders.GETSET,
name: 'totalPower',
get: function() {
// This runs when accessed, not during entity creation
return this.strength + this.intelligence;
}
});
const factory = factory.create({
name: 'character',
props: ['strength', 'intelligence'],
default: { strength: 10, intelligence: 10 },
extend: ['computedProperty']
});
const char = factory.create({ strength: 20 });
console.log(char.totalPower); // 30 (works because getter runs after properties are set)If extenders depend on each other, order them carefully:
factory.registerExtender('initArray', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.items = [];
}
});
factory.registerExtender('addDefaultItems', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.items.push('default-item');
}
});
// Correct order
const factory1 = factory.create({
name: 'example',
extend: ['initArray', 'addDefaultItems'] // initArray first
});
// Wrong order - will crash
const factory2 = factory.create({
name: 'example',
extend: ['addDefaultItems', 'initArray'] // Error: items is undefined
});