tessl install tessl/npm-ecos@0.2.0Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern
Extenders allow you to customize entity creation by adding property accessors (getters/setters) or running initialization functions. This guide covers how to use extenders effectively in your ECOS applications.
ECOS provides two types of extenders:
GETSET extenders allow you to create computed properties, validated properties, or properties with side effects.
const { factory, extenders } = require('ecos');
factory.registerExtender('fullName', {
type: extenders.GETSET,
name: 'fullName',
get: function() {
return `${this.firstName} ${this.lastName}`;
},
set: function(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1] || '';
}
});const personFactory = factory.create({
name: 'person',
props: ['firstName', 'lastName'],
extend: ['fullName']
});const person = personFactory.create({ firstName: 'John', lastName: 'Doe' });
console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'Omit the set function to create read-only properties:
factory.registerExtender('experienceToNext', {
type: extenders.GETSET,
name: 'experienceToNext',
get: function() {
return this.level * 100 - this.experience;
}
// No setter - read-only
});Use setters to validate and constrain values:
factory.registerExtender('validatedLevel', {
type: extenders.GETSET,
name: 'level',
get: function() {
return this._level || 1;
},
set: function(value) {
// Clamp level between 1 and 100
this._level = Math.max(1, Math.min(100, value));
}
});
const playerFactory = factory.create({
name: 'player',
extend: ['validatedLevel']
});
const player = playerFactory.create();
player.level = 200; // Clamped to 100
console.log(player.level); // 100Trigger actions when properties change:
factory.registerExtender('temperature', {
type: extenders.GETSET,
name: 'celsius',
get: function() {
return this._celsius || 0;
},
set: function(value) {
this._celsius = value;
this._fahrenheit = (value * 9/5) + 32;
}
});
factory.registerExtender('fahrenheit', {
type: extenders.GETSET,
name: 'fahrenheit',
get: function() {
return this._fahrenheit || 32;
}
});
const weatherFactory = factory.create({
name: 'weather',
extend: ['temperature', 'fahrenheit']
});
const weather = weatherFactory.create();
weather.celsius = 25;
console.log(weather.fahrenheit); // 77 (automatically calculated)FUNCTION extenders execute custom initialization logic when an entity is created.
const { factory, extenders } = require('ecos');
factory.registerExtender('timestamps', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.createdAt = Date.now();
entity.updatedAt = Date.now();
}
});const entityFactory = factory.create({
name: 'entity',
props: ['name'],
extend: ['timestamps']
});const entity = entityFactory.create({ name: 'MyEntity' });
console.log(entity.createdAt); // Current timestamp
console.log(entity.updatedAt); // Current timestampfactory.registerExtender('uuid', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.uuid = Math.random().toString(36).substr(2, 9);
}
});factory.registerExtender('collections', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.tags = [];
entity.metadata = {};
}
});Access the extender configuration to customize behavior:
factory.registerExtender('randomize', {
type: extenders.FUNCTION,
min: 1,
max: 100,
handler: function(entity, extender) {
const range = extender.max - extender.min;
entity.randomValue = Math.floor(Math.random() * range) + extender.min;
}
});factory.registerExtender('initializeGame', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.stats = {
health: 100,
mana: 50,
stamina: 100
};
entity.inventory = [];
entity.equipment = {
weapon: null,
armor: null,
accessory: null
};
}
});Use both types together for powerful entity initialization:
const { factory, extenders } = require('ecos');
// Initialize internal state with FUNCTION
factory.registerExtender('init', {
type: extenders.FUNCTION,
handler: function(entity) {
entity._internalValue = 0;
entity.logs = [];
}
});
// Add controlled access with GETSET
factory.registerExtender('valueAccessor', {
type: extenders.GETSET,
name: 'value',
get: function() {
return this._internalValue;
},
set: function(val) {
this.logs.push({
timestamp: Date.now(),
oldValue: this._internalValue,
newValue: val
});
this._internalValue = val;
}
});
const trackedFactory = factory.create({
name: 'tracked',
extend: ['init', 'valueAccessor']
});
const entity = trackedFactory.create();
entity.value = 10;
entity.value = 20;
entity.value = 15;
console.log(entity.value); // 15
console.log(entity.logs); // Array with 3 change recordsSee Execution Order for detailed information about when extenders run during entity creation.
Key point: Extenders execute BEFORE properties are assigned. Properties from default or create(options) are NOT yet available in extender handlers.
Choose clear, descriptive names for extenders that indicate their purpose:
// Good
factory.registerExtender('validateEmail', {...});
factory.registerExtender('computeFullName', {...});
factory.registerExtender('initializeInventory', {...});
// Avoid
factory.registerExtender('ex1', {...});
factory.registerExtender('helper', {...});Each extender should have a single, clear responsibility:
// Good - separate concerns
factory.registerExtender('timestamps', {...});
factory.registerExtender('uuid', {...});
// Avoid - too many responsibilities
factory.registerExtender('initEverything', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.createdAt = Date.now();
entity.uuid = Math.random().toString(36).substr(2, 9);
entity.tags = [];
// ... many more things
}
});Since extenders run before properties are assigned, use methods when you need access to default values or create options:
// Register a method for property-dependent initialization
factory.registerMethod('initializeStats', function() {
this.stats = {
health: this.baseHealth || 100,
mana: this.baseMana || 50
};
});
const factory = factory.create({
name: 'character',
props: ['baseHealth', 'baseMana'],
default: { baseHealth: 150, baseMana: 75 },
methods: ['initializeStats']
});
const character = factory.create({ name: 'Hero' });
character.initializeStats(); // Call after creation when properties are availableRegister extenders once and reuse them across multiple factories:
// Register once
factory.registerExtender('timestamps', {...});
factory.registerExtender('uuid', {...});
// Use in multiple factories
const userFactory = factory.create({
name: 'user',
extend: ['timestamps', 'uuid']
});
const postFactory = factory.create({
name: 'post',
extend: ['timestamps', 'uuid']
});The factory module throws errors for invalid extender registration:
try {
factory.registerExtender('myExtender', {
type: extenders.FUNCTION,
handler: function(entity) {
// ... initialization logic
}
});
} catch (error) {
console.error('Failed to register extender:', error.message);
}