tessl install tessl/npm-ecos@0.2.0Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern
Complete API documentation for the entities module, which provides centralized storage and retrieval for all entities.
const { entities } = require('ecos');The entities module is a unified container that stores all entities created by factories. It provides ID-based access, automatic ID assignment, and centralized lifecycle management. This design prevents memory leaks by maintaining a single reference point for all entities.
Stores an entity in the container and assigns it a unique ID. This function is called automatically when entities are created via factory instances.
/**
* Stores an entity in the container and assigns a unique ID
* @param {object} obj - Entity object to store
* @returns {object} The same object with an 'id' property added
* @throws {Error} If obj is not a valid object
*/
entities.set(obj);obj (object, required)
id property addedThe same object passed in, with an id property added. The ID is an auto-incrementing integer starting from 0.
id propertyremoveAll() is calledconst { entities } = require('ecos');
// Manually store an object (typically done automatically by factories)
const entity = { type: 'custom', value: 42 };
entities.set(entity);
console.log(entity.id); // 0
const entity2 = { type: 'custom', value: 100 };
entities.set(entity2);
console.log(entity2.id); // 1const { factory, entities } = require('ecos');
const itemFactory = factory.create({
name: 'item',
props: ['name']
});
// entities.set() is called automatically
const sword = itemFactory.create({ name: 'Sword' });
console.log(sword.id); // 0
const shield = itemFactory.create({ name: 'Shield' });
console.log(shield.id); // 1obj is null, undefined, or not an objectobj is a primitive valueentities.set() manuallyRetrieves an entity from the container by its ID.
/**
* Retrieves an entity from the container by ID
* @param {number} id - Entity ID
* @returns {object|null} Entity object or null if not found
*/
entities.get(id);id (number, required)
entities.set()null if no entity exists with the specified IDconst { factory, entities } = require('ecos');
const factory1 = factory.create({
name: 'item',
props: ['name']
});
const sword = factory1.create({ name: 'Sword' });
const shield = factory1.create({ name: 'Shield' });
console.log(sword.id); // 0
console.log(shield.id); // 1
// Retrieve by ID
const retrieved = entities.get(sword.id);
console.log(retrieved === sword); // true
console.log(retrieved.name); // 'Sword'
// Non-existent ID returns null
console.log(entities.get(999)); // nullconst nodeFactory = factory.create({
name: 'node',
props: ['data', 'childIds']
});
// Create parent and children
const parent = nodeFactory.create({ data: 'root', childIds: [] });
const child1 = nodeFactory.create({ data: 'child1', childIds: [] });
const child2 = nodeFactory.create({ data: 'child2', childIds: [] });
// Store child IDs (not direct references)
parent.childIds = [child1.id, child2.id];
// Retrieve children through container
parent.childIds.forEach(childId => {
const child = entities.get(childId);
console.log(child.data);
});
// Output:
// child1
// child2const entityFactory = factory.create({
name: 'entity',
props: ['type', 'data']
});
// Create various entities
const entities_list = [
entityFactory.create({ type: 'player', data: { name: 'Alice' } }),
entityFactory.create({ type: 'enemy', data: { name: 'Goblin' } }),
entityFactory.create({ type: 'item', data: { name: 'Potion' } })
];
// Store IDs in different categories
const playerIds = [0];
const enemyIds = [1];
const itemIds = [2];
// Retrieve by category
playerIds.forEach(id => {
const player = entities.get(id);
console.log('Player:', player.data.name);
});null for removed or non-existent IDsRemoves an entity from the container by its ID. After removal, subsequent get() calls with that ID will return null.
/**
* Removes an entity from the container
* @param {number} id - Entity ID to remove
*/
entities.remove(id);id (number, required)
Nothing (undefined)
get(id) calls return nullremoveAll() is calledconst { factory, entities } = require('ecos');
const factory1 = factory.create({
name: 'temp',
props: ['data']
});
const entity = factory1.create({ data: 'temporary' });
console.log(entities.get(entity.id)); // { type: 'temp', data: 'temporary', id: 0 }
// Remove the entity
entities.remove(entity.id);
// No longer accessible
console.log(entities.get(entity.id)); // nullconst entityFactory = factory.create({
name: 'entity',
props: ['data', 'childIds']
});
const parent = entityFactory.create({ data: 'parent', childIds: [] });
const child1 = entityFactory.create({ data: 'child1', childIds: [] });
const child2 = entityFactory.create({ data: 'child2', childIds: [] });
parent.childIds = [child1.id, child2.id];
// Remove parent and all children
function removeEntityTree(entityId) {
const entity = entities.get(entityId);
if (!entity) return;
// Remove children first
if (entity.childIds) {
entity.childIds.forEach(childId => removeEntityTree(childId));
}
// Remove self
entities.remove(entityId);
}
removeEntityTree(parent.id);
// All entities removed
console.log(entities.get(parent.id)); // null
console.log(entities.get(child1.id)); // null
console.log(entities.get(child2.id)); // null// Safe removal even if ID doesn't exist
entities.remove(999); // No error thrown
// Safe removal with check
function safeRemove(entityId) {
const entity = entities.get(entityId);
if (entity) {
// Do cleanup
console.log(`Removing entity ${entityId}`);
entities.remove(entityId);
}
}Returns the current number of entities in the container.
/**
* Returns the number of entities in the container
* @returns {number} Entity count
*/
entities.size();Number of entities currently stored in the container (integer >= 0).
const { factory, entities } = require('ecos');
console.log(entities.size()); // 0
const factory1 = factory.create({
name: 'entity',
props: ['value']
});
factory1.create({ value: 1 });
console.log(entities.size()); // 1
factory1.create({ value: 2 });
factory1.create({ value: 3 });
console.log(entities.size()); // 3
entities.remove(0);
console.log(entities.size()); // 2function logEntityCount() {
console.log(`Current entities: ${entities.size()}`);
}
const itemFactory = factory.create({
name: 'item',
props: ['name']
});
logEntityCount(); // Current entities: 0
const item1 = itemFactory.create({ name: 'Item1' });
logEntityCount(); // Current entities: 1
const item2 = itemFactory.create({ name: 'Item2' });
logEntityCount(); // Current entities: 2
entities.remove(item1.id);
logEntityCount(); // Current entities: 1const MAX_ENTITIES = 1000;
function canCreateEntity() {
return entities.size() < MAX_ENTITIES;
}
if (canCreateEntity()) {
const entity = someFactory.create();
} else {
console.log('Entity limit reached!');
}removeAll()Removes all entities from the container and resets the ID counter to 0. After calling removeAll(), the next entity created will have id: 0.
/**
* Removes all entities from the container and resets the ID counter
*/
entities.removeAll();Nothing (undefined)
id: 0const { factory, entities } = require('ecos');
const factory1 = factory.create({
name: 'entity',
props: ['value']
});
const entity1 = factory1.create({ value: 1 });
const entity2 = factory1.create({ value: 2 });
const entity3 = factory1.create({ value: 3 });
console.log(entity1.id); // 0
console.log(entity2.id); // 1
console.log(entity3.id); // 2
console.log(entities.size()); // 3
// Clear all entities
entities.removeAll();
console.log(entities.size()); // 0
console.log(entities.get(entity1.id)); // null
// Next entity starts from ID 0 again
const newEntity = factory1.create({ value: 100 });
console.log(newEntity.id); // 0// Test setup
function setupTest() {
entities.removeAll(); // Clear previous state
factory.reset(); // Clear factory registrations
}
// Test
function testEntityCreation() {
setupTest();
const testFactory = factory.create({
name: 'test',
props: ['value']
});
const entity = testFactory.create({ value: 42 });
console.log(entity.id); // Always 0 (predictable)
// Assertions...
}
// Test teardown
function teardownTest() {
entities.removeAll();
}function loadNewLevel(levelData) {
// Clear previous level entities
entities.removeAll();
// Load new level
levelData.entities.forEach(entityDef => {
const entity = someFactory.create(entityDef);
// Process entity...
});
console.log(`Loaded ${entities.size()} entities for new level`);
}Store child entity IDs in parent entities rather than direct references. This enables proper cleanup and garbage collection.
const { factory, entities } = require('ecos');
const nodeFactory = factory.create({
name: 'node',
props: ['data', 'childIds', 'parentId']
});
// Create hierarchy
const parent = nodeFactory.create({ data: 'root', childIds: [], parentId: null });
const child1 = nodeFactory.create({ data: 'child1', childIds: [], parentId: parent.id });
const child2 = nodeFactory.create({ data: 'child2', childIds: [], parentId: parent.id });
// Store IDs, not references
parent.childIds = [child1.id, child2.id];
// Access children through container
function getChildren(parentEntity) {
return parentEntity.childIds.map(id => entities.get(id)).filter(Boolean);
}
const children = getChildren(parent);
console.log(children.map(c => c.data)); // ['child1', 'child2']Organize entities by type or category using ID arrays.
const gameState = {
playerIds: [],
enemyIds: [],
itemIds: []
};
const entityFactory = factory.create({
name: 'entity',
props: ['category', 'data']
});
// Create entities
const player = entityFactory.create({ category: 'player', data: { name: 'Alice' } });
const enemy1 = entityFactory.create({ category: 'enemy', data: { name: 'Goblin' } });
const enemy2 = entityFactory.create({ category: 'enemy', data: { name: 'Orc' } });
const item = entityFactory.create({ category: 'item', data: { name: 'Sword' } });
// Organize by category
gameState.playerIds.push(player.id);
gameState.enemyIds.push(enemy1.id, enemy2.id);
gameState.itemIds.push(item.id);
// Process by category
function processEnemies() {
gameState.enemyIds.forEach(id => {
const enemy = entities.get(id);
if (enemy) {
console.log('Processing enemy:', enemy.data.name);
}
});
}Properly remove entities and their relationships.
function removeEntityAndChildren(entityId) {
const entity = entities.get(entityId);
if (!entity) return;
// Remove children recursively
if (entity.childIds && entity.childIds.length > 0) {
entity.childIds.forEach(childId => {
removeEntityAndChildren(childId);
});
}
// Remove from parent's child list
if (entity.parentId !== null) {
const parent = entities.get(entity.parentId);
if (parent && parent.childIds) {
parent.childIds = parent.childIds.filter(id => id !== entityId);
}
}
// Remove entity itself
entities.remove(entityId);
}Iterate over all entities (note: requires tracking IDs separately).
// Track all entity IDs
const allEntityIds = new Set();
// Wrap factory create to track IDs
function createTrackedEntity(factoryInstance, options) {
const entity = factoryInstance.create(options);
allEntityIds.add(entity.id);
return entity;
}
// Custom remove to untrack IDs
function removeTrackedEntity(entityId) {
entities.remove(entityId);
allEntityIds.delete(entityId);
}
// Iterate over all entities
function forEachEntity(callback) {
allEntityIds.forEach(id => {
const entity = entities.get(id);
if (entity) {
callback(entity);
}
});
}
// Usage
forEachEntity(entity => {
console.log(`Entity ${entity.id}: ${entity.type}`);
});Save and restore entity container state.
function saveEntitiesState() {
const state = {
entities: [],
nextId: entities.size()
};
// Note: Requires tracking IDs separately (see iteration pattern)
allEntityIds.forEach(id => {
const entity = entities.get(id);
if (entity) {
state.entities.push({ ...entity });
}
});
return state;
}
function loadEntitiesState(state) {
entities.removeAll();
state.entities.forEach(entityData => {
const entity = { ...entityData };
entities.set(entity);
});
}All entities are stored in a single container, preventing memory leaks from orphaned references. When an entity is removed from the container, it can be garbage collected if no other references exist.
Entities should reference each other using IDs rather than direct object references. This pattern:
Entities created through factory instances are automatically registered in the container. Manual use of entities.set() is typically not needed.
Entity IDs are assigned sequentially starting from 0. IDs are never reused unless the container is cleared with removeAll(). This provides:
const { factory, entities } = require('ecos');
// Create factory
const nodeFactory = factory.create({
name: 'node',
props: ['value', 'childIds', 'parentId'],
default: { childIds: [], parentId: null }
});
// Create tree structure
const root = nodeFactory.create({ value: 'root' });
const child1 = nodeFactory.create({ value: 'child1', parentId: root.id });
const child2 = nodeFactory.create({ value: 'child2', parentId: root.id });
const grandchild = nodeFactory.create({ value: 'grandchild', parentId: child1.id });
// Build relationships
root.childIds = [child1.id, child2.id];
child1.childIds = [grandchild.id];
// Access entities
console.log(entities.size()); // 4
console.log(entities.get(root.id).value); // 'root'
// Traverse tree
function printTree(nodeId, depth = 0) {
const node = entities.get(nodeId);
if (!node) return;
console.log(' '.repeat(depth) + node.value);
node.childIds.forEach(childId => printTree(childId, depth + 1));
}
printTree(root.id);
// Output:
// root
// child1
// grandchild
// child2
// Cleanup
function removeTree(nodeId) {
const node = entities.get(nodeId);
if (!node) return;
node.childIds.forEach(childId => removeTree(childId));
entities.remove(nodeId);
}
removeTree(root.id);
console.log(entities.size()); // 0