or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/ecos@0.2.x

docs

advanced

constraints.mdexecution-order.mdpatterns.md
index.md
tile.json

tessl/npm-ecos

tessl install tessl/npm-ecos@0.2.0

Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern

constraints.mddocs/advanced/

Constraints and Limitations

This guide documents all important limitations, gotchas, and constraints in ECOS. Understanding these constraints helps you avoid common pitfalls and design better entity systems.

Critical Constraints

1. Extenders Execute Before Properties Are Assigned

The most important constraint: Extenders run before properties from default or create(options) are available.

const { factory, extenders } = require('ecos');

// ✗ WRONG: This will not work as expected
factory.registerExtender('badExample', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        // baseHealth is undefined here!
        entity.maxHealth = entity.baseHealth * 2;
    }
});

const factory1 = factory.create({
    name: 'character',
    props: ['baseHealth'],
    default: { baseHealth: 100 },
    extend: ['badExample']
});

const char = factory1.create();
console.log(char.maxHealth); // NaN (undefined * 2)

Solutions:

  1. Use methods for property-dependent logic:
factory.registerMethod('computeMaxHealth', function() {
    this.maxHealth = this.baseHealth * 2;
});

const factory2 = factory.create({
    name: 'character',
    props: ['baseHealth'],
    default: { baseHealth: 100 },
    methods: ['computeMaxHealth']
});

const char2 = factory2.create();
char2.computeMaxHealth();
console.log(char2.maxHealth); // 200
  1. Use GETSET extenders (getters run when accessed, not during creation):
factory.registerExtender('maxHealthGetter', {
    type: extenders.GETSET,
    name: 'maxHealth',
    get: function() {
        return this.baseHealth * 2;
    }
});

const factory3 = factory.create({
    name: 'character',
    props: ['baseHealth'],
    default: { baseHealth: 100 },
    extend: ['maxHealthGetter']
});

const char3 = factory3.create();
console.log(char3.maxHealth); // 200 (getter accesses current baseHealth)

See Execution Order for details.

2. Preset Props Must Exist in Factory Default

Critical limitation: Preset props do NOT add new properties to entities. They only initialize properties that already exist in the factory's default option.

// ✗ WRONG: Will cause TypeError at runtime
factory.registerPreset('positioned', {
    props: ['x', 'y']
});

const badFactory = factory.create({
    name: 'entity',
    presets: ['positioned']
    // Missing default: { x: 0, y: 0 } - TypeError will occur!
});

// ✓ CORRECT: Props from preset are in default
const goodFactory = factory.create({
    name: 'entity',
    presets: ['positioned'],
    default: { x: 0, y: 0 }  // Required!
});

Why this happens: Preset props are a design quirk of ECOS. They don't actually add properties; they only specify which properties should be initialized during entity creation. If those properties don't exist in default, the initialization fails.

Best practice: Always ensure factory default includes all properties referenced in preset props:

// Register preset with props
factory.registerPreset('mortal', {
    props: ['health', 'maxHealth'],
    methods: ['takeDamage']
});

// Factory MUST include these props in default
const characterFactory = factory.create({
    name: 'character',
    presets: ['mortal'],
    default: {
        health: 100,     // Required
        maxHealth: 100   // Required
    }
});

3. Entity IDs Are Auto-Incrementing

Entity IDs are sequential integers starting from 0. You cannot specify custom IDs.

const { factory, entities } = require('ecos');

const myFactory = factory.create({
    name: 'entity',
    props: ['value']
});

const entity1 = myFactory.create({ value: 1 });
const entity2 = myFactory.create({ value: 2 });
const entity3 = myFactory.create({ value: 3 });

console.log(entity1.id); // 0
console.log(entity2.id); // 1
console.log(entity3.id); // 2

// Cannot set custom ID
entity1.id = 999; // This changes the property but doesn't update the container
console.log(entities.get(999)); // null (entity is still at ID 0)
console.log(entities.get(0));   // entity1 (ID 0 still works)

Implications:

  • IDs are predictable in order of creation
  • IDs are NOT reused when entities are removed (unless container is cleared with removeAll())
  • Don't try to assign custom IDs; use a separate property if needed
// Use a separate property for custom identifiers
factory.registerExtender('customId', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.uuid = Math.random().toString(36).substr(2, 9);
    }
});

const entityFactory = factory.create({
    name: 'entity',
    extend: ['customId']
});

const entity = entityFactory.create();
console.log(entity.id);   // 0 (auto-assigned)
console.log(entity.uuid); // 'a1b2c3d4e' (custom)

4. Duplicate Registration Errors

Once registered, methods, extenders, and presets cannot be re-registered with the same name:

// First registration succeeds
factory.registerMethod('move', function(dx, dy) {
    this.x += dx;
    this.y += dy;
});

// Second registration fails
try {
    factory.registerMethod('move', function(dx, dy) {
        // Different implementation
        this.x += dx * 2;
        this.y += dy * 2;
    });
} catch (error) {
    console.error(error.message); // "Method 'move' already registered"
}

Solution: Use factory.reset() to clear registrations (useful for testing):

factory.reset(); // Clear all registrations

// Now can re-register
factory.registerMethod('move', function(dx, dy) {
    this.x += dx * 2;
    this.y += dy * 2;
});

Warning: factory.reset() clears ALL methods, extenders, and presets. Use with caution.

5. Methods Must Be Registered Before Factory Creation

You must register methods before creating a factory that references them:

// ✗ WRONG: Method not registered yet
try {
    const factory1 = factory.create({
        name: 'entity',
        methods: ['move']  // Error: 'move' not registered
    });
} catch (error) {
    console.error(error.message);
}

// ✓ CORRECT: Register method first
factory.registerMethod('move', function(dx, dy) {
    this.x += dx;
    this.y += dy;
});

const factory2 = factory.create({
    name: 'entity',
    methods: ['move']  // Works
});

Same applies to extenders and presets:

// Register first
factory.registerExtender('timestamps', { ... });
factory.registerPreset('positioned', { ... });

// Then use in factory
const myFactory = factory.create({
    name: 'entity',
    extend: ['timestamps'],
    presets: ['positioned']
});

Entity Container Constraints

1. No Direct Container Access

The entities container is a Map internally, but you can only access it through provided methods:

const { entities } = require('ecos');

// ✓ Available operations
entities.get(id);
entities.remove(id);
entities.size();
entities.removeAll();

// ✗ Not available
// entities.forEach(...)
// entities.keys()
// entities.values()
// entities.entries()

Implication: You cannot iterate over all entities. If you need iteration, maintain your own list:

const allEntities = [];

const myFactory = factory.create({
    name: 'entity',
    props: ['value']
});

const entity1 = myFactory.create({ value: 1 });
const entity2 = myFactory.create({ value: 2 });

allEntities.push(entity1);
allEntities.push(entity2);

// Now can iterate
allEntities.forEach(entity => {
    console.log(entity.value);
});

2. Entities Not Automatically Removed

Entities remain in the container until explicitly removed:

const { factory, entities } = require('ecos');

const myFactory = factory.create({
    name: 'entity',
    props: ['value']
});

const entity = myFactory.create({ value: 42 });
console.log(entities.size()); // 1

// Setting to null doesn't remove from container
entity = null;
console.log(entities.size()); // Still 1

// Must explicitly remove
entities.remove(entity.id);
console.log(entities.size()); // 0

Best practice: Implement cleanup methods for entities with children:

factory.registerMethod('destroy', function() {
    // Clean up child entities
    if (this.childIds) {
        this.childIds.forEach(id => entities.remove(id));
    }

    // Remove self
    entities.remove(this.id);
});

3. ID Reset Only with removeAll

The ID counter only resets to 0 when calling entities.removeAll():

const { factory, entities } = require('ecos');

const myFactory = factory.create({ name: 'entity' });

// Create and remove
const entity1 = myFactory.create();
console.log(entity1.id); // 0

entities.remove(entity1.id);

// Next entity still has ID 1
const entity2 = myFactory.create();
console.log(entity2.id); // 1 (not 0)

// removeAll resets counter
entities.removeAll();

const entity3 = myFactory.create();
console.log(entity3.id); // 0 (reset)

Property and Type Constraints

1. Properties Initialized to Null

Properties from props are initialized to null if not in default or create(options):

const myFactory = factory.create({
    name: 'entity',
    props: ['a', 'b', 'c'],
    default: { a: 1 }
});

const entity = myFactory.create({ b: 2 });
console.log(entity.a); // 1 (from default)
console.log(entity.b); // 2 (from create options)
console.log(entity.c); // null (not specified)

Gotcha: If you expect properties to have a specific default value, include them in default:

// ✗ Unexpected null
const factory1 = factory.create({
    name: 'entity',
    props: ['count']
});
const entity1 = factory1.create();
console.log(entity1.count + 1); // NaN (null + 1)

// ✓ Expected default
const factory2 = factory.create({
    name: 'entity',
    props: ['count'],
    default: { count: 0 }
});
const entity2 = factory2.create();
console.log(entity2.count + 1); // 1

2. Type Property is Read-Only in Practice

The type property is set during creation. Changing it doesn't affect container storage:

const myFactory = factory.create({ name: 'entity' });
const entity = myFactory.create();

console.log(entity.type); // 'entity'

entity.type = 'different';
console.log(entity.type); // 'different' (changed)

// But this doesn't change how it's stored
// The entity is still stored as type 'entity'

Best practice: Don't modify the type property. Treat it as read-only.

3. Method Name Conflicts

Method names cannot conflict with existing entity properties:

const myFactory = factory.create({
    name: 'entity',
    props: ['id', 'type']
});

// ✗ Cannot register method named 'id' or 'type'
factory.registerMethod('id', function() {
    return this.id;
}); // Error: conflicts with entity property

Design Constraints

1. No Prototype Inheritance

Entities don't use prototype inheritance. Methods are attached directly to each entity:

const myFactory = factory.create({
    name: 'entity',
    methods: ['method1']
});

const entity1 = myFactory.create();
const entity2 = myFactory.create();

console.log(entity1.method1 === entity2.method1); // false
// Each entity has its own method reference

Implication: While this prevents prototype chain issues, it means each entity has its own copy of each method. For thousands of entities, this can use more memory than prototype-based approaches.

2. No Direct Entity-to-Entity References

Best practice: Store entity IDs, not direct references:

const { factory, entities } = require('ecos');

const myFactory = factory.create({
    name: 'entity',
    props: ['data']
});

const parent = myFactory.create({ data: 'parent' });
const child = myFactory.create({ data: 'child' });

// ✗ Not recommended: direct reference
parent.child = child;

// ✓ Recommended: store ID
parent.childId = child.id;

// Later, access via container
const retrievedChild = entities.get(parent.childId);

Why: Direct references prevent proper garbage collection when entities are removed from the container.

3. No Built-in Query System

ECOS doesn't provide methods to query entities by type, property values, etc:

// ✗ Not available
// entities.findByType('player')
// entities.where({ health: 100 })
// entities.filter(entity => entity.active)

Solution: Implement your own query system if needed:

class EntityManager {
    constructor() {
        this.entitiesByType = new Map();
    }

    create(factory, options) {
        const entity = factory.create(options);

        // Index by type
        if (!this.entitiesByType.has(entity.type)) {
            this.entitiesByType.set(entity.type, []);
        }
        this.entitiesByType.get(entity.type).push(entity.id);

        return entity;
    }

    getByType(type) {
        const ids = this.entitiesByType.get(type) || [];
        return ids.map(id => entities.get(id)).filter(Boolean);
    }
}

Testing Constraints

1. Shared Global State

The factory and entities modules maintain global state:

const { factory, entities } = require('ecos');

// Registration persists across test runs
factory.registerMethod('testMethod', function() {});

// Entities persist across test runs
const myFactory = factory.create({ name: 'test' });
myFactory.create();
console.log(entities.size()); // 1

Solution: Reset state between tests:

beforeEach(() => {
    entities.removeAll();  // Clear entities
    factory.reset();       // Clear registrations
});

2. ID Predictability in Tests

Entity IDs start from 0, making them predictable in tests:

const { factory, entities } = require('ecos');

beforeEach(() => {
    entities.removeAll(); // Reset IDs to 0
});

test('entity creation', () => {
    const myFactory = factory.create({ name: 'test' });

    const entity = myFactory.create();
    expect(entity.id).toBe(0); // Predictable
});

Error Handling Constraints

1. Limited Error Context

Errors from ECOS don't always provide detailed context:

try {
    const factory1 = factory.create({
        name: 'entity',
        methods: ['nonExistent']
    });
} catch (error) {
    console.log(error.message);
    // May not tell you which method was not found
}

Best practice: Validate configuration before creating factories:

function validateFactory(config) {
    if (config.methods) {
        config.methods.forEach(methodName => {
            // Check if method exists
            // Throw descriptive error if not
        });
    }
    return factory.create(config);
}

2. No Warning for Unused Configuration

ECOS doesn't warn about unused configuration:

// No error or warning for typo
const myFactory = factory.create({
    name: 'entity',
    porps: ['value']  // Typo: should be 'props'
});

const entity = myFactory.create();
// entity doesn't have 'value' property, no warning

Best practice: Use TypeScript or validate configurations to catch typos.

See Also