API reference
koota-kit, except the React bindings, which are only available from koota-kit/react so the root entry never loads React. The module-specific import paths shown below can reduce coupling in larger codebases.All functions and types are exported from koota-kit, except the React
bindings, which are only available from koota-kit/react so the root entry
never loads React. The module-specific import paths shown below can reduce
coupling in larger codebases.
spatial
Import from koota-kit/spatial or the root:
function queryPointClearance(
provider: SpatialProvider,
oracle: SpatialNearestSurfaceOracle,
query: SpatialClearanceQuery,
): SpatialClearanceResult;queryPointClearance decides one explicit predicate: the query point is at
least minimumDistance from every selected open surface. The default
selection is each part's full topology; callers must explicitly request
visible topology. A provider record carries stable source, instance, part,
and provider-local registration identity; a common Euclidean frame and unit;
independent geometry, topology, pose, and transform revisions; and certified
enclosing AABBs for both topology selections.
The core performs deterministic AABB broadphase only. It invokes the supplied
nearest-surface oracle for overlapping candidates and validates that its
witness is in the selected certified bound, in the same frame/unit, and was
computed from the exact consumed identity/topology/revision tuple. Results
include those checked tuples, ordered witnesses, exclusions, one of
satisfied, violated, unknown, unsupported, or stale, and an
exactness label. An AABB rejection can conservatively satisfy the threshold;
an oracle witness or a stamped bounded no-hit is exact. Satisfaction that
combines AABB and oracle evidence is conservative. A stale expected/provider
tuple wins before the result can be trusted; otherwise an exact violating
witness wins over an unavailable or foreign-frame peer. Sparse provider
snapshots and capability lists fail closed. Validated query thresholds are captured before any
provider or oracle callback and cannot change during evaluation.
There is no geometry kernel, BVH, renderer, model, physics, Koota, or React dependency in this entry. It does not prove containment or solid nonpenetration: an open-surface nearest query has no watertightness/thickness semantics. A future solid query needs an injected containment-capable oracle.
world
createSimWorld(seeds)
function createSimWorld(seeds: RngSeeds): WorldHandle;Creates a Koota World, two seeded RNG streams, immutable copied seeds, a
zeroed clock, and an empty scratch map. Seeds must be strings or finite
numbers. Validation happens before the Koota world is allocated.
destroySimWorld(handle)
function destroySimWorld(handle: WorldHandle): void;Clears scratch state and destroys the Koota world if it is still active. The operation is idempotent.
advanceClock(handle, dt)
function advanceClock(handle: WorldHandle, dt: number): void;Advances tickIndex by one and adds dt to simSeconds. dt must be finite
and non-negative. A zero-duration tick is allowed. Overflow and corrupted clock
values raise RangeError without changing the clock.
snapshotWorld(handle) / restoreWorldHeader(handle, snapshot)
function snapshotWorld(handle: WorldHandle): WorldSnapshot;
function restoreWorldHeader(handle: WorldHandle, snapshot: WorldSnapshot): void;Snapshots and restores both RNG streams plus the simulation clock. The plain
object can be round-tripped through JSON. It does not include WorldHandle.seeds:
persist the immutable seeds separately and use them when creating a fresh
handle for loading. Entity and trait data are also outside this package's
persistence boundary. Restore validates the complete replacement before
mutating the handle.
Koota exports
createActions, relation, and trait are re-exported so simulation code can
keep its Koota imports at one boundary. Entity and World are exported as
types.
rng
type RngSeeds = {
readonly gen: string | number;
readonly events: string | number;
};
function createRng(seeds: RngSeeds): RngLayers;
function nextU32(stream: RngStream): number;
function nextFloat(stream: RngStream): number;
function nextInt(stream: RngStream, minInclusive: number, maxExclusive: number): number;
function chance(stream: RngStream, probability: number): boolean;
function pick<T>(stream: RngStream, items: readonly T[]): T;
function shuffle<T>(stream: RngStream, items: readonly T[]): T[];
function snapshotStream(stream: RngStream): RngStreamSnapshot;
function restoreStream(snapshot: RngStreamSnapshot): RngStream;
function snapshotLayers(layers: RngLayers): RngLayersSnapshot;
function restoreLayers(snapshot: RngLayersSnapshot): RngLayers;nextU32returns an unsigned 32-bit integer.nextFloatreturns a value in[0, 1).nextIntuses half-open bounds and exactly one float draw after validation.chanceconsumes one draw for every valid probability, including0and1, keeping later draw positions predictable.pickreturns one element uniformly with exactly onenextIntdraw and rejects an empty array without drawing.shufflereturns a Fisher–Yates shuffled copy (the input is never mutated) and consumes exactlylength - 1draws, so the draw count depends only on the length, never on the contents.- Restore functions reject malformed ARC4 state and clone accepted state so a snapshot can seed multiple independent replays.
Named substreams
type Substreams = {
readonly seed: string | number;
readonly streams: ReadonlyMap<string, RngStream>;
};
type SubstreamsSnapshot = {
seed: string | number;
streams: Record<string, RngStreamSnapshot>;
};
function substream(seed: string | number, name: string): RngStream;
function createSubstreams(seed: string | number): Substreams;
function getSubstream(substreams: Substreams, name: string): RngStream;
function snapshotSubstreams(substreams: Substreams): SubstreamsSnapshot;
function restoreSubstreams(snapshot: SubstreamsSnapshot): Substreams;substream(seed, name)returns a fresh stream seeded byderiveSeed(seed, name). Its sequence depends only onseedandname, so adding, removing, reordering, or consuming another substream never shifts it.namemust be a non-empty string. Invalid input throws before anything is allocated.createSubstreamsholds one lazily created stream per name, so generators that share a set keep drawing where they left off.getSubstreamreturns the same stream instance for the same name.snapshotSubstreamscaptures the seed plus every stream created so far, with names in sorted order.restoreSubstreamsvalidates the whole snapshot before building anything and reproduces every later draw byte-exact. Streams that were never created restore lazily from the seed.
seed
type SeedPart = string | number;
type EntropySource = {
getRandomValues(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
}; // `globalThis.crypto` satisfies it, with or without DOM typings
const MASTER_SEED_BYTES = 16;
function deriveSeed(parent: SeedPart, ...parts: SeedPart[]): string;
function createMasterSeed(entropy?: EntropySource): string;
function isMasterSeed(value: unknown): value is string;deriveSeedhashes the parent seed and every part with SHA-256 and returns the first 128 bits as 32 lowercase hex characters. The result is a valid seed forcreateRng,createSimWorld,substream, and furtherderiveSeedcalls.- Every value is length-prefixed before hashing, so
("a", "bc")and("ab", "c")derive different seeds. Order matters. - Numbers canonicalize to their string form, matching
createRng, soderiveSeed(7, "x") === deriveSeed("7", "x"). Non-finite numbers and non-string, non-number values throw. - The output is stable across platforms and package versions. A change to the derivation would be a breaking change.
createMasterSeedfills 16 bytes fromentropy(defaultglobalThis.crypto) and returns them as 32 lowercase hex characters. It throws when no entropy source exists, when the source returns something other than the requested 16-byte array, or when it returns all zero bytes, which signals an unfilled buffer rather than real entropy.isMasterSeedaccepts exactly 32 lowercase hex characters. Use it to validate a persisted seed before deriving from it.
traits
function defineTrait<S extends Schema>(schema: S & SafeSchema<S>): Trait<Norm<S>>;Pass primitive fields directly. Wrap each object- or array-valued field in a factory, or use a whole-trait factory:
const Health = defineTrait({ value: 100 });
const Path = defineTrait({ points: () => [] as Array<{ x: number; y: number }> });
const Pose = defineTrait(() => ({ position: { x: 0, y: 0 } }));Bare object and array fields are rejected by SafeSchema and by the runtime
guard.
schedule
Import from koota-kit/schedule or the root.
type System = (handle: WorldHandle, dt: number) => void;
type ScheduleOptions = {
step: number; // seconds per step; finite, > 0
maxSteps: number; // safe integer >= 1: most steps one tick may run
key?: string; // scratch key for the accumulator; unique by default
};
type Schedule = {
readonly step: number;
readonly maxSteps: number;
readonly key: string;
tick(handle: WorldHandle, frameDt: number): number;
remainder(handle: WorldHandle): number;
};
function createSchedule(systems: readonly System[], options: ScheduleOptions): Readonly<Schedule>;createSchedule copies systems and validates step, maxSteps, key and
every system before returning; bad configuration throws TypeError or
RangeError naming createSchedule.
tick(handle, frameDt) adds frameDt to an accumulator stored in
handle.scratch under key, then, while at least one whole step is owed and
fewer than maxSteps have run, calls advanceClock(handle, step) and then
every system in order with dt === step. It returns the number of steps run.
Systems read dt, never the frame's dt, so the same elapsed time gives the same
run at any frame rate, provided no single tick owes more than maxSteps (the
excess is dropped, so one long frame and many short ones then differ).
- A
frameDtthat is negative,NaNor infinite counts as0.0is a valid pause: no step runs and the clock does not move, even if a failed tick left whole steps owed; those wait for the next frame that moves time. - When
maxStepsis reached with whole steps still owed, those steps are dropped, not carried. The sub-step fraction is kept, so phase is preserved. - Owed time saturates at the largest finite double instead of overflowing.
- A billionth of a step of tolerance absorbs float drift, so 432 frames of 1/144 s run the same 180 steps as 180 frames of 1/60 s.
- The accumulator lives in
scratch, so it dies with the world and two handles never share one. Akeyalready holding something other than a schedule accumulator throwsTypeErrorbefore the clock moves. Two schedules given the same explicitkeyshare one accumulator, but an explicitkeymay not equal another schedule's generated key, and a generated key skips any explicit one. - If a system throws, the step it was in is already spent (the clock moved) and the steps not yet started stay owed, so a retry does not replay spent time.
remainder(handle) returns the seconds currently owed, in [0, step) between
ticks (0 on a handle that has not ticked). remainder / step is the
interpolation alpha for a renderer drawing between sim states.
eventLog
type EventLog<T> = {
readonly key: string;
push(handle: WorldHandle, event: T): void;
drain(handle: WorldHandle): T[];
peek(handle: WorldHandle): readonly T[];
clear(handle: WorldHandle): void;
size(handle: WorldHandle): number;
};
function defineEventLog<T>(key: string): EventLog<T>;Logs are created once at module scope but store their arrays in each handle's
scratch map. drain truncates the live array in place so an existing internal
reference is not orphaned. Empty reads do not create a scratch entry. peek
returns a detached array; size does not allocate.
coordination
Import from koota-kit/coordination or the root. This React-free entry binds
to an existing caller-owned Koota World; it never creates, resets, or
destroys that World or its entities.
type CompletedRevisions = Readonly<{
geometry: number;
pose: number;
transform: number;
}>;
type CompletedMetadata = Readonly<{ revisions: CompletedRevisions }>;
type CoordinationDescriptor = Readonly<{
id: string;
capabilities: readonly string[];
}>;
type CoordinationUpdate = Readonly<{
token: RegistrationToken;
completed: CompletedMetadata;
}>;
type CoordinationEntry = Readonly<{
id: string;
capabilities: readonly string[];
completed: CompletedMetadata | undefined;
}>;
type CoordinationSnapshot = Readonly<{
revision: number;
entries: readonly CoordinationEntry[];
}>;
function bindCoordination(world: World): Coordination;id is a caller-authored, non-empty stable string. It is not a packed Koota
entity ID. Capability labels are copied, sorted, and must be unique non-empty
strings; change a capability set by unregistering and registering again.
completed.revisions.geometry, .pose, and .transform are all required
non-negative safe integers. They may advance independently but no dimension
may decrease. A new completed value with all three values equal is a no-op.
type Coordination = Readonly<{
readonly world: World;
register(entity: Entity, descriptor: CoordinationDescriptor): RegistrationToken;
publish(token: RegistrationToken, completed: CompletedMetadata): CoordinationSnapshot;
publishBatch(updates: readonly CoordinationUpdate[]): CoordinationSnapshot;
unregister(token: RegistrationToken): CoordinationSnapshot;
prune(): CoordinationSnapshot;
getRegistration(id: string): RegistrationToken | undefined;
getEntity(id: string): Entity | undefined;
getSnapshot(): CoordinationSnapshot;
subscribe(listener: (snapshot: CoordinationSnapshot) => void): () => void;
dispose(): void;
}>;register accepts only a live entity in the supplied World; cross-World,
destroyed, duplicated, and externally conflicting entities are rejected before
the coordinator mutates its projection. The opaque token is scoped to one
binding and to the entity generation. publish and unregister reject a
stale token. If an entity is destroyed or the owned registration trait is
removed externally, call prune() to publish its removal before reusing the
stable ID. If Koota rejects the binding marker during construction or rejects an
add callback during register, the binding removes only its just-added marker or
trait before rethrowing, so an existing peer binding remains intact and the
caller may retry registration. getEntity and getRegistration return undefined for a stale
registration.
Use one Koota runtime and module format per process. Koota
0.6.6 replaces numeric entity dispatch when its opposite ESM/CommonJS format
loads. Binding rejects a foreign peer World before adding its marker, and
attests the existing marker holder to detect contamination that predates the
first kit import. Later live reads, mutations, and cleanup throw a
coordination: dispatch-realm error if that attestation fails; they preserve
the last committed snapshot and subscribers. getSnapshot() remains readable.
A subscriber that changes dispatch after publication triggers an error after
all listeners receive the newly committed snapshot; that publication stays
committed. An opposite-format koota-kit/spatial import is safe because that entry does
not load Koota. This guard detects the known peer-format conflict; it does not
repair global methods or make mixed-format entity operations interoperable.
publishBatch validates every token and metadata value before it changes any
owned trait or snapshot. Its completed projection changes only after every raw
Koota trait write succeeds; a Koota callback error can leave partial raw mirror
writes, which are outside that projection. A completed snapshot is immutable,
detached from the caller input, and sorted by stable ID. Producers that need a
cross-producer atomic cohort must publish through the same binding; disjoint
bindings are independent cohorts. revision is a binding-local committed
publication generation, not a geometry, pose, transform, source-hash, or
provider freshness claim. It deliberately does not serialize entities or join
the RNG/clock WorldSnapshot boundary.
Subscribers each receive the same committed snapshot. A subscribe or
unsubscribe during delivery affects only a later publication. Registry mutation
during delivery or an owned Koota trait callback throws. Listener failures are
reported after every listener has been offered the committed snapshot; they do
not roll back it. When a listener throws during register, use
getRegistration(id) to recover the successfully committed token.
dispose is idempotent. It removes only this binding's registration traits and
World-held ownership marker, keeps other bindings' markers, and never destroys
or resets the borrowed World, its entities, caller traits, providers, or
buffers. Several bindings may share a World only for disjoint entities.
CoordinationBinding and CoordinationRegistration are the exported Koota
trait identities for this module format. The binding trait holds only opaque
per-binding ownership markers on the borrowed World; it is removed when the
last binding disposes. Direct trait mutation is outside the immutable committed-
snapshot contract; use the binding methods for publication. The module has no
renderer, React, producer, geometry, spatial-index, or AI dependency. Exact
adapters must own their own immutable buffers or read leases and report their
own source, topology, frame, unit, and freshness contracts.
react
Import from koota-kit/react. Requires react (>=18), an optional peer
dependency; no other entry point imports it.
type SimWorldProviderProps = {
readonly handle: WorldHandle;
readonly children?: ReactNode;
};
const SimWorldProvider: FunctionComponent<SimWorldProviderProps>;
function useSimWorld(): WorldHandle;SimWorldProvider supplies handle to the tree below it and renders
koota/react's WorldProvider for handle.world, so useWorld, useTrait,
useQuery and useActions work under it without a second provider.
useSimWorld returns the provided handle, including its rng, seeds,
clock and scratch, and throws an Error when no SimWorldProvider is above
it. Passing a different handle re-provides every consumer. The provider does
not create or destroy the handle: pair createSimWorld with destroySimWorld
yourself.