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

arch-nexus-ecs

v0.9.0

Published

Arch Nexus Entity Component System

Readme

Arch Nexus ECS

Arch-Nexus-ECS is a lightweight Entity Component System (ECS) library for TypeScript, built on an archetype-based storage model for cache-efficient iteration.

Installation

npm install arch-nexus-ecs
# or
bun add arch-nexus-ecs

Core concepts

| Concept | Role | |---|---| | Entity | Unique ID that ties components together | | Component | Pure data — no logic | | System | Logic that runs every frame, queries for entities | | World | Central hub: manages entities, systems, events, and resources | | Archetype | Internal grouping of entities that share the same component set |


Components

Two styles work interchangeably:

import { component, Component } from "arch-nexus-ecs";

// decorator style (no base class needed)
@component
class Position {
  constructor(public x: number, public y: number) {}
}

@component
class Velocity {
  constructor(public x: number, public y: number) {}
}

// extends style (still works)
class Health extends Component {
  constructor(public value: number) { super(); }
}

Systems

Class-based system

import { System, World, Query } from "arch-nexus-ecs";

class MoveSystem extends System {
  private q!: Query<[typeof Position, typeof Velocity], []>;

  startUp(world: World) {
    this.q = world.createQuery<[typeof Position, typeof Velocity], []>()
      .with(Position, Velocity);
  }

  update(world: World) {
    for (const [pos, vel] of this.q.findAll()) {
      pos.x += vel.x;
      pos.y += vel.y;
    }
  }
}

Function-based system with defineSystem

import { defineSystem, query, queryAll, queryFirst, resource, subscribe } from "arch-nexus-ecs";

class GameConfig {
  constructor(public speed: number) {}
}

@event
class PlayerDied {
  constructor(public entityId: number) {}
}

const moveSystem = defineSystem(
  {
    movers:  queryAll(Position, Velocity),  // ResultTuple<T>[] — iterate directly
    leader:  queryFirst(Position),          // ResultTuple<T> | undefined
    config:  resource(GameConfig),          // Resource<T> | undefined
    deaths:  subscribe(PlayerDied),         // EventBuffer<T> — events this frame
  },
  ({ movers, leader, config, deaths }) => {
    const speed = config?.get().speed ?? 1;

    for (const [pos, vel] of movers) {     // no .findAll() needed
      pos.x += vel.x * speed;
      pos.y += vel.y * speed;
    }

    if (leader) {
      const [pos] = leader;                // no .findFirst() needed
      console.log("leader at", pos.x);
    }

    for (const e of deaths) {
      console.log("player died:", e.entityId);
    }
  }
);

All descriptors are resolved automatically on startUp and are fully typed:

| Descriptor | Resolved type | Notes | |---|---|---| | query(A, B) | Query<[typeof A, typeof B], []> | cached query object, call .findAll() / .findFirst() | | queryAll(A, B) | ResultTuple<[A, B]>[] | refreshed every frame, iterate directly | | queryFirst(A, B) | ResultTuple<[A, B]> \| undefined | refreshed every frame | | resource(T) | Resource<T> \| undefined | | | subscribe(E) | EventBuffer<E> | cleared after each update() |

All descriptors support .without() for exclusions:

queryAll(Position).without(Dead)
queryFirst(Position, Velocity).without(Frozen)
query(Position).without(Velocity)

Lifecycle hooks

const sys = defineSystem(
  { movers: queryAll(Position, Velocity) },
  ({ movers }) => { /* update — runs every frame */ },
  {
    startUp: ({ movers }, world) => { /* runs once after descriptors are resolved */ },
    destroy: ({ movers }, world) => { /* runs on world.destroy() */ },
  }
);

Queries

Query object (class systems or manual use)

const q = world.createQuery().with(Position, Velocity);
const alive = world.createQuery().with(Position).without(Dead);

q.findAll();   // ResultTuple<T>[]
q.findFirst(); // ResultTuple<T> | undefined
q.findLast();  // ResultTuple<T> | undefined

Convenience methods (one-off queries)

// shorthand — creates and executes a query in one call, no caching
const all   = world.queryAll(Position, Velocity);   // ResultTuple<T>[]
const first = world.queryFirst(Position, Velocity); // ResultTuple<T> | undefined

For queries that run every frame, prefer query() / queryAll() / queryFirst() descriptors inside defineSystem — they cache the query object and only re-execute when archetypes change.

Include Entity in the query to access the entity ID:

const q = world.createQuery().with(Entity, Position);
for (const [entity, pos] of q.findAll()) {
  console.log(entity.id, pos.x);
}

World setup

import { World } from "arch-nexus-ecs";

const world = new World();

// add resources before startUp
world.addResource(new GameConfig(2));

// register systems (class or function-based)
world.addSystems(SpawnSystem, MoveSystem);
world.addSystem(moveSystem);

world.startUp();

// game loop
const loop = () => {
  world.update();
  setTimeout(loop, 16); // ~60fps
};
loop();

Entity lifecycle

const e = world.addEntity(new Position(0, 0), new Velocity(1, 0));

world.addComponentToEntity(e, new Health(100));
world.removeComponentFromEntity(e, new Velocity(0, 0));
world.removeEntity(e);

Events

import { event } from "arch-nexus-ecs";

@event
class PlayerDied {
  constructor(public entityId: number) {}
}

// subscribe
world.addSubscriber(PlayerDied, (e: PlayerDied) => {
  console.log("player died:", e.entityId);
});

// dispatch
world.addEvent(new PlayerDied(42));

// unsubscribe
world.unsubscribe(PlayerDied, handler);

To handle events inside defineSystem, use the subscribe() descriptor — it buffers events per frame and cleans up automatically on destroy().


Resources

Global singleton data accessible from any system:

class Score {
  constructor(public value: number) {}
}

world.addResource(new Score(0));

const score = world.getResource(Score); // Resource<Score> | undefined
score?.get().value; // number

world.removeResource(Score);

Task Scheduler

Run time-sequenced async tasks inside the game loop:

import { WaitAmountOfSeconds } from "arch-nexus-ecs";

function* spawnWave(world: World) {
  yield new WaitAmountOfSeconds(3);
  world.addEntity(new Position(0, 0), new Velocity(1, 0));
  yield new WaitAmountOfSeconds(3);
  world.addEntity(new Position(5, 0), new Velocity(-1, 0));
}

world.addTaskScheduler(spawnWave);

The generator function itself is the identifier — no strings, no manual instantiation:

world.pauseTaskScheduler(spawnWave);   // pauses without destroying the generator
world.resumeTaskScheduler(spawnWave);  // resumes from where it stopped
world.removeTaskScheduler(spawnWave);  // stops and removes from the scheduler list

Plugins

Bundle groups of systems and resources into reusable plugins:

import { IPlugin, World } from "arch-nexus-ecs";

class PhysicsPlugin implements IPlugin {
  build(world: World) {
    world.addResource(new PhysicsConfig());
    world.addSystems(CollisionSystem, GravitySystem);
  }
}

world.addPlugin(PhysicsPlugin);

Scripts

bun test          # run tests
bun run typecheck # type-check without emitting
bun run build     # full tsc build

License

MIT