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

koota-kit

v0.9.11

Published

Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.

Readme

koota-kit

Two deterministic streams—world generation and runtime events—flow through a simulation clock into modular ECS components.

CI MIT license

Small, production-derived conventions for deterministic simulations built with Koota. koota-kit gives a simulation one lifecycle boundary, two independent seeded random streams, safe object-valued traits, and world-scoped event logs.

It is deliberately not a game engine. It is the narrow layer that prevents recurring classes of simulation bug without hiding Koota's queries, entities, relations, or actions.

Full documentation: jonbogaty.com/koota-kit

Why use it?

| Problem | koota-kit convention | | --- | --- | | World generation changes after unrelated gameplay draws | Separate gen and events RNG streams | | A loaded save no longer reproduces the next random result | Byte-exact, JSON-safe RNG snapshots | | Object state leaks between entities | defineTrait requires per-entity factories | | A HUD or test consumes an event before its real system sees it | Single-consumer drain, non-consuming peek | | Replaced worlds retain caches or process-global Koota IDs | createSimWorld / idempotent destroySimWorld lifecycle | | Two producers publish half-updated model facts | koota-kit/coordination commits an immutable completed metadata projection |

Install

pnpm add koota-kit koota

Requirements:

  • Node.js 22, 24 and 26 (CI covers each on Linux and Node 24 on Windows). The package declares >=22; support follows maintained Node lines rather than requiring one exact patch release.
  • Koota ^0.6.6, installed by the application as a peer dependency
  • React >=18, only if you import koota-kit/react; it is an optional peer dependency, and no other entry point loads it

The package ships native ESM and CommonJS entry points with format-correct TypeScript declarations.

Quick start

import {
  advanceClock,
  createSimWorld,
  defineEventLog,
  defineTrait,
  destroySimWorld,
  nextInt,
  snapshotWorld,
} from "koota-kit";

const Position = defineTrait({ x: 0, y: 0 });
const Inventory = defineTrait({ items: () => [] as string[] });
const moved = defineEventLog<{ entity: number; distance: number }>("movement:completed");

const sim = createSimWorld({ gen: "map-42", events: "run-1" });

try {
  const scout = sim.world.spawn(Position({ x: 4, y: 9 }), Inventory());

  advanceClock(sim, 1 / 60);
  const distance = nextInt(sim.rng.events, 1, 5);
  const position = scout.get(Position);
  if (position) scout.set(Position, { ...position, x: position.x + distance });
  moved.push(sim, { entity: Number(scout), distance });

  // The owning system consumes each event once.
  for (const event of moved.drain(sim)) {
    console.log(event);
  }

  // Save this beside your own serialized entity/trait state.
  const header = snapshotWorld(sim);
  const serializedHeader = JSON.stringify(header);
  console.log(serializedHeader);
} finally {
  destroySimWorld(sim);
}

Runnable ESM and CommonJS versions live in examples/.

Core concepts

One handle owns one simulation

createSimWorld creates a Koota world, immutable seed values, independent RNG streams, a fixed-step clock, and a scratch map. destroySimWorld clears the scratch map and releases Koota's world ID; it is safe to call more than once.

The scratch map is for derived, non-serializable caches and short-lived coordination state. Namespace keys by system, for example pathfinding:grid or combat:hits.

Coordinate existing model entities without another World

koota-kit/coordination binds to a caller-owned raw Koota World; it does not create, reset, or destroy one. Register existing entities with stable caller-authored IDs and capability labels, then publish completed producer revisions as a batch. The snapshot is immutable, ordered by ID, and changes only after the complete batch validates and its raw Koota trait writes succeed. Producers needing one atomic cohort publish through the same binding; disjoint bindings are independent cohorts.

import { bindCoordination } from "koota-kit/coordination";

const coordination = bindCoordination(sim.world);
const ava = sim.world.spawn(Position({ x: 4, y: 9 }));
const token = coordination.register(ava, {
  id: "figure:ava",
  capabilities: ["presence", "geometry"],
});

coordination.publish(token, {
  revisions: { geometry: 12, pose: 8, transform: 31 },
});

const completed = coordination.getSnapshot();
coordination.dispose(); // removes only koota-kit's coordination traits

The three producer revisions are independent values; a coordination snapshot revision only identifies a committed observer view. This entry does not copy or freeze provider buffers, choose a geometry backend, evaluate a pose, or answer spatial queries. Those facts belong to later producer/query adapters. Call prune() after an entity is removed externally before reusing its stable ID. Listener errors occur after a committed publication; retrieve a token with getRegistration(id) if registration notification threw before register could return it.

Use one Koota runtime and module format per process. Koota 0.6.6 replaces numeric entity dispatch when the opposite ESM/CommonJS peer format loads. Coordination then rejects live reads, writes, and cleanup with a dispatch-realm error; getSnapshot() still returns its last committed view. Raw callback writes may already have happened, but failed cleanup never reports successful disposal. The independent koota-kit/spatial entry can be imported in either format.

The caller composition proof documents the optional registry consumer, copied figure inputs, immutable geometry captures and the limits of open-surface integration evidence.

Randomness has two jobs

  • rng.gen defines stable world content: terrain, layouts, initial spawns.
  • rng.events defines play history: combat rolls, weather, runtime events.

Never key per-entity randomness on packed Koota entity IDs or consume a shared stream in query iteration order. Entity IDs contain process-global world and generation bits. Derive a local stream from a stable domain key instead:

import { nextU32, substream } from "koota-kit/rng";
import { deriveSeed } from "koota-kit/seed";

const tileSeed = deriveSeed(sim.seeds.gen, "tile", 12, 8);
const terrainVariant = nextU32(substream(tileSeed, "terrain"));

deriveSeed hashes length-prefixed parts into a 128-bit hex seed, so seed chains such as master seed → run → level never collide by accident. substream(seed, name) gives each generator its own stream; adding a new named substream never shifts an existing one. createMasterSeed() makes a 128-bit root seed from crypto.getRandomValues, and accepts an injected entropy source for tests.

Object-valued traits need factories

Koota's structure-of-arrays layout can alias a bare object literal across entities. koota-kit rejects that form at compile time and runtime:

defineTrait({ storage: () => ({ logs: 0 }) }); // correct: one object per entity
defineTrait({ storage: { logs: 0 } });         // TypeError: shared object footgun

Declare traits once in a leaf module and export each trait from one location. Koota identifies traits by reference, not by a string name.

A fixed-step schedule removes the frame rate from the sim

createSchedule(systems, { step, maxSteps }) returns a tick(handle, frameDt) for your frame loop. The frame's dt fills an accumulator kept in handle.scratch (so it dies with the run); each whole step advances the clock, then calls every system in order with dt === step. At most maxSteps run per tick and any excess is dropped, so one huge dt after a backgrounded tab cannot cause a catch-up storm. tick(handle, 0) is a valid pause and returns the steps run.

import { createSchedule } from "koota-kit";

const schedule = createSchedule([stove, mixer, serving], { step: 1 / 60, maxSteps: 6 });
// once per rendered frame:
const steps = schedule.tick(sim, frameDt);

Sixty frames of 1/60 s and thirty of 1/30 s run the same sixty steps. schedule.remainder(sim) / schedule.step is the interpolation alpha.

Event logs have one consuming owner

push appends, drain returns every pending event and empties the log, and peek returns a copy without consuming. A devtool, HUD, or test should peek; the system responsible for acting on the event should drain.

Persistence boundary

snapshotWorld stores the clock and both RNG states. It intentionally does not serialize immutable seeds, entities, or traits: persist sim.seeds beside the header so a fresh handle can retain stable derived randomness, and keep entity data in application-specific schemas and migrations. Stage a fresh handle when loading so old scratch caches and event logs cannot leak into the restored run. restoreWorldHeader validates a parsed save before changing that staged handle and restores atomically.

Persist a version beside the header and your ECS state so the application can migrate its own schema.

React

koota/react hooks reach only the Koota World. koota-kit/react adds a provider for the whole WorldHandle, so components can also reach its RNG streams, seeds, clock and scratch:

import { useQuery } from "koota/react";
import { createSimWorld, defineTrait, nextInt } from "koota-kit";
import { SimWorldProvider, useSimWorld } from "koota-kit/react";

const Position = defineTrait({ x: 0, y: 0 });
const handle = createSimWorld({ gen: "world-7", events: "run-7" });

function Scouts() {
  const { rng } = useSimWorld(); // the handle: rng, seeds, clock, scratch
  const scouts = useQuery(Position); // koota/react hooks share the same provider
  return (
    <button type="button" onClick={() => nextInt(rng.events, 1, 7)}>
      Roll for {scouts.length} scouts
    </button>
  );
}

root.render(
  <SimWorldProvider handle={handle}>
    <Scouts />
  </SimWorldProvider>,
);

SimWorldProvider renders Koota's own WorldProvider for handle.world, so useTrait, useQuery and useActions need no second provider. useSimWorld() throws outside a provider. Passing a different handle re-provides every consumer; the provider does not create or destroy the handle, so pair createSimWorld with destroySimWorld yourself.

API and architecture

The root package exports the full API except the React bindings, which live only at koota-kit/react so the root never loads React. Focused entry points are also available:

| Entry point | Purpose | | --- | --- | | koota-kit/world | World lifecycle, clock, Koota facade, world snapshots | | koota-kit/rng | Seeded streams, named substreams, bounded draws, snapshots | | koota-kit/seed | Seed derivation and 128-bit master seeds | | koota-kit/traits | Safe trait declaration | | koota-kit/schedule | Fixed-step system schedule with a step cap | | koota-kit/eventLog | World-scoped publish/drain logs | | koota-kit/spatial | Renderer-neutral point-to-open-surface clearance queries | | koota-kit/react | SimWorldProvider and useSimWorld (optional React peer) |

See the API reference for signatures and validation behavior, and architecture notes for module boundaries and invariants.

For task-oriented usage, read the Sourcey guides on deterministic randomness, save headers, and event ownership.

Errors and edge cases

  • Seeds must be strings or finite numbers. Substream names must be non-empty strings.
  • createMasterSeed throws when no entropy source exists or the source leaves the buffer all zero.
  • advanceClock accepts finite, non-negative dt; 0 is a valid paused tick.
  • createSchedule needs a finite step > 0 and a safe-integer maxSteps >= 1; a negative, NaN or infinite frameDt counts as 0.
  • queryPointClearance accepts only finite coordinates and a shared Euclidean frame/unit. It proves distance to declared open surfaces, never containment or solid nonpenetration.
  • nextInt requires safe-integer bounds with maxExclusive > minInclusive and a range no larger than 2**32.
  • chance accepts only finite probabilities in [0, 1].
  • Restores reject malformed RNG or clock state before mutating the handle.
  • Event-log keys must be non-empty and cannot reuse a scratch key holding a non-array cache.

Invalid caller input raises TypeError or RangeError with the API name in the message. No validation failure consumes RNG state or partially restores a world header.

Development

mise install   # or: corepack enable
pnpm install --frozen-lockfile
pnpm verify

Node and pnpm versions live in mise.toml; CI verifies against the full supported Node range (22, 24, 26).

pnpm verify runs formatting/lint checks, strict TypeScript, the full test suite with 100% coverage thresholds, dual-format builds, both runnable examples, publint, Are The Types Wrong, and package-content/runtime checks.

Useful individual commands:

pnpm test             # fast behavior suite
pnpm test:watch       # local watch mode
pnpm coverage         # tests plus enforced coverage thresholds
pnpm build            # dist/esm and dist/cjs
pnpm package:check    # exports, declarations, packed files, ESM/CJS parity
pnpm audit --prod     # registry-backed production dependency audit

Releases and support

Release Please uses Conventional Commits to select versions and GitHub's merged-pull-request release notes to maintain CHANGELOG.md. A release tag triggers a clean verification and npm publication with provenance. The latest 0.x release receives fixes; pre-1.0 APIs may evolve, with breaking changes called out in the changelog.

See CONTRIBUTING.md before opening a pull request. Report security issues privately according to SECURITY.md.

License

MIT © Jon Bogaty.