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

execution-order.mddocs/advanced/

Execution Order

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.

Overview

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.

The Entity Creation Flow

Complete Sequence

When an entity is created, operations occur in this order:

  1. Entity object creation: Base object is created with type property
  2. ID assignment: Entity is registered in the container and assigned a unique id
  3. Preset extenders: Extenders from presets are applied
  4. Factory extenders: Extenders from factory's extend option are applied
  5. Property initialization: Properties from props array are initialized to null
  6. Default values: Properties from factory's default option are assigned
  7. Create options: Properties from create(options) are assigned (overriding defaults)
  8. Method attachment: Methods are attached to the entity

Detailed Walkthrough

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.

Critical Timing Considerations

Properties Not Available in Extenders

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)

Correct Approach: Use Methods

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); // 300

Alternative: Initialize in Extenders, Compute Later

Initialize 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 }

Extender Execution Order

Within a Single Factory

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']

Preset Extenders Before Factory Extenders

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']

Multiple Presets

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']

Property Assignment Order

Props Initialize to Null

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 }

Defaults Override 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 }

Create Options Override Defaults

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 }

Method Attachment

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 Constructor Timing

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

Visualizing the Flow

┌─────────────────────────────────────────────┐
│ 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         │
       └───────────────────────┘

Practical Implications

1. Extenders for Structure, Methods for Logic

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
});

2. GETSET Extenders Don't Need Property Values

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)

3. Initialization Order Matters

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
});

See Also

  • Working with Extenders - Practical extender patterns
  • Constraints - Limitations and gotchas
  • Patterns - Design patterns that work with execution order