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

@oasys/oecs

v0.2.1

Published

Archetype-based Entity Component System

Readme

OECS

A fast, minimal archetype-based Entity Component System written in TypeScript.

  • Structure-of-Arrays (SoA) — each component field is a contiguous typed array column (Float64Array, Int32Array, etc.), enabling cache-friendly inner loops.
  • Phantom-typed componentsComponentDef<{x:"f64",y:"f64"}> is just a number at runtime, but enforces field names and types at compile time.
  • Batch iterationfor..of over a query yields non-empty archetypes. Access SoA columns via get_column() and write the inner loop.
  • Single-entity refsctx.ref(Pos, entity) gives you a cached accessor with prototype-backed getters/setters.
  • Resources — typed global singletons (time, input, config) with live readers.
  • Events & signals — fire-and-forget SoA channels, auto-cleared each frame.
  • Deferred structural changes — add/remove component and destroy entity are buffered during system execution and flushed between phases.
  • Topological system ordering — systems within a phase are sorted by before/after constraints using Kahn's algorithm.
  • Fixed timestep — configurable fixed update loop with accumulator and interpolation alpha.

Installation

Using npm (or equivalent):

npm install @oasys/oecs

Quick start

import { ECS, SCHEDULE } from "@oasys/oecs";

const world = new ECS();

// Record syntax — per-field type control
const Pos = world.register_component({ x: "f64", y: "f64" });

// Array shorthand — uniform type, defaults to "f64"
const Vel = world.register_component(["vx", "vy"] as const);

// Tags have no fields
const IsEnemy = world.register_tag();

// Resources are global singletons
const Time = world.register_resource(["delta", "elapsed"] as const, {
  delta: 0,
  elapsed: 0,
});

// Events are fire-and-forget messages
const Damage = world.register_event(["target", "amount"] as const);

// Create entities and attach components
const e = world.create_entity();
world.add_component(e, Pos, { x: 0, y: 0 });
world.add_component(e, Vel, { vx: 100, vy: 50 });
world.add_component(e, IsEnemy);

// Register a system with a typed query
const moveSys = world.register_system(
  (q, _ctx, dt) => {
    for (const arch of q) {
      const px = arch.get_column(Pos, "x");
      const py = arch.get_column(Pos, "y");
      const vx = arch.get_column(Vel, "vx");
      const vy = arch.get_column(Vel, "vy");
      const n = arch.entity_count;
      for (let i = 0; i < n; i++) {
        px[i] += vx[i] * dt;
        py[i] += vy[i] * dt;
      }
    }
  },
  (qb) => qb.every(Pos, Vel),
);

// Schedule the system
world.add_systems(SCHEDULE.UPDATE, moveSys);

// Initialize
world.startup();

// Game loop
function frame(dt: number) {
  world.set_resource(Time, { delta: dt, elapsed: performance.now() / 1000 });
  world.update(dt);
}

World Options

ECS accepts an optional configuration object:

const world = new ECS({
  initial_capacity: 4096,  // pre-allocate archetype storage (default: 1024, grows automatically)
  fixed_timestep: 1 / 50,  // fixed update interval in seconds (default: 1/60)
  max_fixed_steps: 4,       // cap fixed updates per frame to prevent spiral of death (default: 5)
});

| Option | Type | Default | Description | |---|---|---|---| | initial_capacity | number | 1024 | Starting size for every archetype's backing typed arrays (entity IDs and all SoA component columns). Arrays double when exceeded — set this close to your expected entity count per archetype to avoid early re-allocations. | | fixed_timestep | number | 1/60 | Interval (seconds) for FIXED_UPDATE systems. | | max_fixed_steps | number | 5 | Maximum FIXED_UPDATE iterations per frame. |

Components

Components map field names to typed array tags. All field values are number, but storage uses the specified typed array (Float64Array, Int32Array, etc.) for cache-friendly iteration.

// Record syntax — per-field type control
const Position = world.register_component({ x: "f64", y: "f64" });
const Health = world.register_component({ current: "i32", max: "i32" });

// Array shorthand — all fields default to "f64"
const Vel = world.register_component(["vx", "vy"] as const);

// Tags — no fields
const IsEnemy = world.register_tag();

Supported typed array tags: "f32", "f64", "i8", "i16", "i32", "u8", "u16", "u32".

Add components individually or via add_components (single archetype transition):

world.add_component(e, Position, { x: 10, y: 20 });
world.add_components(e, [
  { def: Position, values: { x: 10, y: 20 } },
  { def: Health, values: { current: 100, max: 100 } },
]);

See docs/api/components.md for full API.

Queries

Queries are live views over all archetypes matching a component mask.

const q = world.query(Position, Velocity);

// Iterate non-empty archetypes, access SoA columns, write the inner loop
for (const arch of q) {
  const px = arch.get_column(Position, "x");
  const py = arch.get_column(Position, "y");
  const vx = arch.get_column(Velocity, "vx");
  const vy = arch.get_column(Velocity, "vy");
  const n = arch.entity_count;
  for (let i = 0; i < n; i++) {
    px[i] += vx[i];
    py[i] += vy[i];
  }
}

// Chaining
const targets = world
  .query(Position)
  .and(Health)
  .not(Shield)
  .any_of(IsEnemy, IsBoss);

See docs/api/queries.md for full API.

Systems

Systems are plain functions registered with a query and scheduled into lifecycle phases.

// With a typed query
const moveSys = world.register_system(
  (q, ctx, dt) => {
    for (const arch of q) {
      /* ... */
    }
  },
  (qb) => qb.every(Pos, Vel),
);

// Without a query
const logSys = world.register_system({
  fn(ctx, dt) {
    console.log("frame", dt);
  },
});

Inside systems, use ctx for deferred structural changes and per-entity access:

const e = ctx.create_entity();
ctx.add_component(e, Pos, { x: 0, y: 0 });
ctx.destroy_entity(someEntity);
ctx.remove_component(entity, Health);

See docs/api/systems.md for full API.

Resources

Resources are typed global singletons — time, input state, camera config.

const Time = world.register_resource(["delta", "elapsed"] as const, {
  delta: 0,
  elapsed: 0,
});

// Write
world.set_resource(Time, { delta: dt, elapsed: total });

// Read — scalar values, not arrays
const time = world.resource(Time);
time.delta; // number
time.elapsed; // number

See docs/api/resources.md for full API.

Events & Signals

Events are fire-and-forget SoA channels, auto-cleared each frame.

// Data events carry fields
const Damage = world.register_event(["target", "amount"] as const);
ctx.emit(Damage, { target: entityId, amount: 50 });

const dmg = ctx.read(Damage);
for (let i = 0; i < dmg.length; i++) {
  dmg.target[i]; // number
  dmg.amount[i]; // number
}

// Signals carry no data — just a count
const OnReset = world.register_signal();
ctx.emit(OnReset);
if (ctx.read(OnReset).length > 0) {
  /* fired */
}

See docs/api/events.md for full API.

Refs

Refs provide cached single-entity field access — faster than get_field/set_field for repeated access.

const pos = ctx.ref(Pos, entity);
const vel = ctx.ref(Vel, entity);
pos.x += vel.vx * dt;
pos.y += vel.vy * dt;

See docs/api/refs.md for full API.

Schedule

Seven lifecycle phases, executed in order:

| Phase | When | Use case | | -------------- | --------------------- | ----------------------- | | PRE_STARTUP | Once, before startup | Resource loading | | STARTUP | Once | Initial entity spawning | | POST_STARTUP | Once, after startup | Validation | | FIXED_UPDATE | Every tick (fixed dt) | Physics, simulation | | PRE_UPDATE | Every frame, first | Input handling | | UPDATE | Every frame | Game logic | | POST_UPDATE | Every frame, last | Rendering, cleanup |

world.add_systems(SCHEDULE.UPDATE, moveSys, physicsSys);
world.add_systems(SCHEDULE.POST_UPDATE, renderSys);

// Ordering constraints
world.add_systems(SCHEDULE.UPDATE, moveSys, {
  system: physicsSys,
  ordering: { after: [moveSys] },
});

See docs/api/schedule.md for full API.

Entity lifecycle

const e = world.create_entity();
world.is_alive(e); // true
world.destroy_entity_deferred(e); // deferred
world.flush();
world.is_alive(e); // false

Entity IDs are generational: destroying an entity increments its slot's generation, so stale IDs are detected as dead.

Dev / Prod modes

__DEV__ compile-time flags enable bounds checking, dead entity detection, and duplicate system detection. Circular dependency detection is always active (not tree-shaken). All other dev checks are tree-shaken in production builds.

Development

pnpm install
pnpm test          # vitest in watch mode
pnpm bench         # run benchmarks
pnpm build         # vite library build
pnpm tsc --noEmit  # type check

Guides