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

taco-ecs

v0.1.3

Published

Archetype-based Entity-Component-System for TypeScript and JavaScript.

Downloads

14

Readme

taco-ecs

npm version license

taco-ecs is an archetype-based ECS for TypeScript and JavaScript with safe component ownership, strict scheduler validation, explicit structural changes, and dual ESM/CommonJS packaging that is validated through the packed artifact.

It is built for systems where many items move through the same stages:

  • games and game-style simulations
  • agent swarms and rule-driven simulations
  • workflow engines and ticket pipelines
  • service backends with repeatable update phases
  • operational systems that benefit from dense scans over shared state

Installation

npm install taco-ecs

Supported runtime target: Node 18+.

Why this package

  • archetype storage for dense queries
  • typed components, tags, resources, and events
  • explicit structural changes via CommandBuffer
  • strict Schedule validation with startup systems and stage dependencies
  • explicit change tracking via .changed(...) and markChanged(...)
  • packaged for both ESM and CommonJS consumers
  • validated as a real npm artifact through pack and install smoke tests

Module support

ESM:

import { World, defineComponent } from "taco-ecs";

CommonJS:

const { World, defineComponent } = require("taco-ecs");

Core model

taco-ecs is an archetype ECS:

  • entities are 32-bit generational handles
  • components define structural identity and data columns
  • structural changes move entities between archetypes
  • queries iterate matching archetypes and rows

That means add() and remove() are structural operations, not simple field assignments. If you need to record structural work safely while iterating, use CommandBuffer and flush after the pass or let Schedule flush for you.

Quick start

import { World, defineComponent } from "taco-ecs";

const Position = defineComponent({
  name: "Position",
  create: () => ({ x: 0, y: 0 }),
});

const Velocity = defineComponent({
  name: "Velocity",
  create: () => ({ x: 0, y: 0 }),
});

const world = new World();
const entity = world.spawn([
  [Position, { x: 10, y: 20 }],
  [Velocity, { x: 1, y: -2 }],
]);

world.beginFrame();
world.query(Position, Velocity).forEach((e, pos, vel) => {
  pos.x += vel.x;
  pos.y += vel.y;
  world.markChanged(e, Position);
});

console.log(world.get(entity, Position));

Good fits

Use this package when you have a lot of entities or items that:

  • share a small set of component shapes
  • move through repeatable update stages
  • benefit from dense scans rather than object-by-object polymorphism
  • need clear rules for deferred structural edits

If your problem is mostly a few deeply nested custom objects with little shared iteration, an ECS may not be the right fit.

Component ownership

Caller-provided values are cloned when they enter the world through:

  • spawn
  • add
  • set

That keeps external objects from aliasing entity-owned state by accident.

const Position = defineComponent({
  name: "Position",
  create: () => ({ x: 0, y: 0 }),
});

const shared = { x: 1, y: 2 };
const world = new World();
const a = world.spawn([[Position, shared]]);
const b = world.spawn([[Position, shared]]);

world.get(a, Position).x = 99;

console.log(shared.x);                 // 1
console.log(world.get(b, Position).x); // 1

For custom classes or special storage, provide a custom clone function.

const Transform = defineComponent({
  name: "Transform",
  create: () => new Float32Array(16),
  clone: (value) => value.slice(),
});

Change tracking

.changed(Component) is explicit.

A component counts as changed in the current frame when you:

  • call world.set(entity, Component, value), or
  • mutate the live component in place and then call world.markChanged(entity, Component)

Direct in-place mutation without markChanged is visible to plain queries, but it is not visible to .changed(...) filters.

world.beginFrame();
const pos = world.get(entity, Position);
pos.x += 1;
world.markChanged(entity, Position);

world.query(Position).changed(Position).forEach((_e, changedPos) => {
  console.log(changedPos);
});

Reserved handles and command buffers

CommandBuffer.spawn() reserves an entity handle immediately and materializes it on flush().

  • world.isReserved(e) tells you whether a handle is still deferred
  • world.isAlive(e) only becomes true after the buffered spawn is flushed
  • normal component access throws while the entity is reserved
import { CommandBuffer } from "taco-ecs";

const commands = new CommandBuffer(world);
const reserved = commands.spawn([[Position, { x: 1, y: 2 }]]);

console.log(world.isAlive(reserved));    // false
console.log(world.isReserved(reserved)); // true

commands.flush();

console.log(world.isAlive(reserved));    // true
console.log(world.isReserved(reserved)); // false

Schedule behavior

Schedule is intentionally strict.

  • duplicate system names throw
  • duplicate stage names throw
  • unknown stages throw
  • unknown before / after references throw
  • cross-stage dependency references throw
  • dependency cycles throw with the blocked system names

Startup systems are supported through addStartup(name, fn). They run exactly once per Schedule instance, on the first run(world, dt), after world.beginFrame() and before regular stage execution.

import { Schedule } from "taco-ecs";

const schedule = new Schedule()
  .setStageOrder(["pre", "update", "post"])
  .addStartup("bootstrap", ({ commands }) => {
    commands.spawn([[Position, { x: 0, y: 0 }], [Velocity, { x: 1, y: 0 }]]);
  })
  .add("movement", ({ world }) => {
    world.query(Position, Velocity).forEach((e, pos, vel) => {
      pos.x += vel.x;
      pos.y += vel.y;
      world.markChanged(e, Position);
    });
  }, { stage: "update" });

schedule.run(world, 1 / 60);

Resources

import { defineResource } from "taco-ecs";

const Time = defineResource<{ dt: number }>("Time");
world.setResource(Time, { dt: 1 / 60 });
console.log(world.getResource(Time).dt);

Entity handle limits

Entity handles are 32-bit packed values with:

  • ENTITY_INDEX_BITS = 20
  • ENTITY_GENERATION_BITS = 12
  • MAX_ENTITY_COUNT = 1_048_576
  • MAX_ENTITY_GENERATION = 4095

That gives you about one million live entity slots per world before allocation fails. Handles are generational, so stale references become invalid after recycling. The four constants above are exported from the package root.

Secondary capabilities

The package also includes:

  • typed event buffers
  • component add/remove observers
  • world snapshot serialization helpers

Those are useful secondary tools, but the core package pitch is still the ECS runtime: world, queries, structural changes, schedules, and resources.

Examples

From a cloned repository, run npm run build first. The published package already includes built output.

node examples/01_quick_start.mjs
node examples/02_particles.mjs
node examples/03_change_tracking.mjs
node examples/04_schedule_resources.mjs
node examples/05_hierarchy_pattern.mjs
node examples/06_service_pipeline.mjs
node examples/07_ticket_workflow.mjs
node examples/08_workflow_pipeline.mjs

See INTEGRATION.md for guidance on choosing world boundaries, stages, resources, and events for real applications.

Development

npm ci
npm run check
npm test
npm run bench
npm run smoke
npm run examples:smoke
npm run pack:check
npm run install:smoke

Publishing

Release guidance lives in PUBLISHING.md. The repository includes a CI workflow and a trusted-publishing npm release workflow under .github/workflows/.