npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ajeeb/ecs

v0.1.4

Published

Entity Component System

Downloads

27

Readme

Entity Component System

A simple entity component system inspired by EnTT.

Entities

An entity is just an opaque identifier managed by the ECS system. You can create them with create, and under the hood they are just JavaScript integers.

import { Ecs } from '@ajeeb/ecs'
const ecs = new Ecs()
const player = ecs.create()
const enemy = ecs.create()
console.log(player, enemy)
// 0 1

Entities are generational meaning the system will recycle old values whenever possible and encode each entity's generation in the number itself. This is visible when destroying entities with destroy.

ecs.destroy(enemy)
const enemy2 = ecs.create()
console.log(player, enemy, enemy2)
// 0 1 4194305

The exact encoding of generations is an implementation detail you should not depend on -- treat entities as opaque identifiers. You can check the validity of an entity with isValid.

console.log(ecs.isValid(player), ecs.isValid(enemy), ecs.isValid(enemy2))
// true false true

The entity stored in enemy is no longer valid because it was destroyed while the other entities remain valid. This is useful for situations where entity values get stored in different parts of your code and need to check if they've been destroyed.

Components

Entities are just identifiers and contain no data or logic. Data is associated with entities by assigning components. A component is a plain JavaScript object with a constructor that can be used to identify it. Any object that meets that definition can be used, but ES6 classes are ideal.

You assign a component to an entity with assign which has two syntaxes. For simple components you can use a combination of the constructor value and a plain data object.

class Position { x = 0; y = 0 }
ecs.assign(player, Position, { x: 100, y: 200 })
ecs.assign(enemy2, Position, { x: 50 })

Under the hood this calls the constructor with no arguments and uses Object.assign to populate fields. The data object is optional making this syntax convenient for empty components that act as "tags".

class Enemy {}
ecs.assign(enemy2, Enemy)

You can also assign full instances, allowing you to take advantage of custom constructors or factory functions.

class Speed { 
    value = 0  
    constructor(mode) {
        this.value = mode === 'fast' ? 10 : 1
    }
}
ecs.assign(player, new Speed('fast'))

Each entity can only have one component of a given type.

You get components with get and check for their existence with has.

console.log(ecs.get(player, Position))
// Position { x: 100, y: 200 }

console.log(ecs.has(player, Position), ecs.has(player, Enemy))
// true false

Due to them being instances of classes you can write methods for your components if you like.

class Health {
    value = 100

    takeDamage(factor=1) {
        this.value -= Math.random() * factor
    }
}
ecs.assign(player, Health)
ecs.get(player, Health).takeDamage()

Lifecycle

It is sometimes useful to run logic when a component is assigned or removed to or from an entity. Component types can optionally implement the Component interface which exposes the onAssign and onRemove methods. They will be called when the component instance is assigned to an entity or removed from an entity and can be used for setup, cleanup, and resource management.

import { PhysicsEngine } from 'your-physics-engine'
class Physics {
    body
    onAssign(e) {
        PhysicsEngine.addBody(this.body)
    }
    onRemove(e) {
        PhysicsEngine.removeBody(this.body)
    }
}
const physicsObject = ecs.create()

// calls onAssign
ecs.assign(physicsObject, Physics, { body: PhysicsEngine.newBody() })

// calls onRemove
ecs.remove(physicsObject, Physics)

// would also call onRemove
ecs.destroy(physicsObject)

Systems

You can query for entities that has been assigned a set of components using each which returns an optimized generator of matching entities usable in a for...of loop. The order of iteration is undefined and should not be relied on.

// apply gravity to all enemies
for(const e of ecs.each(Position, Enemy)) {
    // e is guaranteed to have at least Position and Enemy components
     ecs.get(e, Position).y -= 0.1
}

Because its just normal iteration you can organize your logic however you want.

function applyEnemyGravity(ecs, amount=0.1) {
    for(const e of ecs.each(Position, Enemy)) {
         ecs.get(e, Position).y -= amount
    }
}

applyEnemyGravity(ecs, 0.5)

Deferring Operations

Three methods make visible mutations to the Ecs instance

  • assign -- assigns a component to an entity
  • remove -- removes a component from an entity
  • destroy -- destroys an entity, removing all components assigned to it

These mutations are visible in iterations using each and when reading with has and get.

By default the changes are immediately visible which is simple and intuitive. There are situations where you might want to defer these operations to take place later, for example at the end of a frame so that all ECS operations during a frame take place against a "stable world".

The three methods above take a defer boolean as their last argument. It is optional and defaults to the value passed into the Ecs constructor, allowing you to configure your ECS to defer by default and/or determine deferral on a per-operation basis.

Deferred operations take effect when you call commit

const engine = new GameEngine()
const ecs = new Ecs({ defer: true })
engine.update = function() {
    for(const e of ecs.each(Position, EnemySpawner)) {
        if(Math.random() < 0.1) {
            const newEnemy = ecs.create()
            ecs.assign(newEnemy, Position, ecs.get(e, Position))
            ecs.assign(newEnemy, Enemy)
        }
    }

    for(const e of ecs.each(Position, Enemy)) {
        // any enemies added by the spawner in the above loop
        // *not visible here* until next frame
    }

    // end of frame
    ecs.commit() // actually add enemies created in first loop
}