aiecsjs Changelog
Planned
Section titled “Planned”- Add
pipeAsyncfor async system composition. - Doc-test harness so README code blocks are mechanically verified.
- Promote
aiecsjs/workertostableonce true SAB shared-memory column aliasing is implemented. - Document the 8-bit generation wrap caveat in STABILITY.md: with the
default
generationBits=8, a single slot recycled 256 times wraps back to its starting generation, briefly re-opening the ABA window. Safe for v0.5 shmup workloads (~5000 frame to wrap a single slot at 60 fps × ~1k destroys); high-churn pools should setcreateWorld({ indexBits: 16, generationBits: 16 })(16 + 16 = 32 bits; 65 536 entities × 65 536 generations). See test tests/ref.test.tsgeneration wrapdescribe block.
[0.5.5] - 2026-06-08
Section titled “[0.5.5] - 2026-06-08”Changed
Section titled “Changed”- Project home migrated to the
isluminaGitHub org; published from there via npm trusted publisher (OIDC + SLSA provenance). Family-wide version alignment at0.5.5.
Internal
Section titled “Internal”- Test version assertions now derive from
package.jsoninstead of a hard-coded string, shrinking the release version-sync surface.
[0.5.3] - 2026-06-05
Section titled “[0.5.3] - 2026-06-05”- Add
forEachEntityIndexedyielding the masked index alongside the EntityId; closes the A1 packed-EntityId footgun in code (safe column iteration is now the default;forEachEntityunchanged).
- Correctness clarification —
forEachEntity’seis a packedEntityId, not a column index. The column-iteration docs/examples now lead withforEachEntityIndexed((e, i, ...cols) => …), indexing SoA columns with the yielded safe indexi(pos.x[i]) — no manual masking (README.md,README_ZHTW.md,docs/MIGRATION.md,docs/MIGRATION_ZHTW.md, regeneratedllms-full.txt).forEachEntityis documented as the form to use when you only need theEntityId; for that raw form, derive the subscript withgetEntityIndex(e). The oldpos.x[e]pattern only worked while generation 0 (e === getEntityIndex(e)); after anydestroyEntityrecycles a slot it read an out-of-bounds column slot. Added a recycle regression test proving both the bug and the fix;e’s packed semantics are intentionally unchanged (it is still what you pass todestroyEntity/hasComponent/command buffers). Corrected the 0.1.0 storage note that impliedPosition.x[eid]indexes by the packed id. - A2 (tag slot footgun). Documented that each tag in
defineQuery([...])occupies a callback slot passed astrue, with a correct mixed-query example; recommend placing tags last so data columns come first. Docs-only — the callback arity is intentionally unchanged (changing it would shift data params for code that correctly writes(e, _tag, pos); deferred to a future major). Column arguments remainany-typed. - A3 (reactive-query lazy arm). Documented that enter/exit buffers are armed lazily on first registration/read, and recommend defining reactive queries at module scope so the first
tickalready observes transitions.
- Synced the embedded
VERSIONconstant (src/version.ts) to0.5.2to matchpackage.json— the release gate asserts they are equal (tests/utils.test.ts). No API change;distdiffers from 0.5.1 only by this version stamp.
0.5.1 - 2026-06-02
Section titled “0.5.1 - 2026-06-02”destroyEntitynow fires the reactiveenterQuery/exitQuerysurface. PreviouslydestroyEntitycleared the entity mask wholesale without notifyingrecordEntityMaskChange, soexitQuery(...)buffers stayed empty on destroy — asymmetric with bothremoveComponent(which notifies) and the query-targetedobserve(q, 'remove')observer (which already fired on destroy). A destroyed entity now records an exit for every query it was matching. The pre-destroy mask is snapshotted at destroy entry so reentrant teardown handlers cannot suppress the exit. No public API change.disposeWorld/destroyWorldnow release the large per-entity arrays. The dispose path cleared archetypes/storages/queries but leftentityMask,entityArchetype,generations,freeList,componentBitFor,bitToQueries,queryArchetypeStamp, andsaballocated. Because the public world handle’scapacitygetter closes over the internal state object, those arrays survived for the lifetime of the (typically retained) handle, defeating the “clear large buffers to help GC” intent. They are now released (typed arrays swapped to length-0 instances, Maps/arrays cleared,sabnulled). Post-dispose operations still throw as before — this is internal state, not public API.unpackBinarybest-effort mode no longer leaks a rawSyntaxError. UnderonUnknownVersion: 'best-effort', a malformed/garbled snapshot body reachingJSON.parsenow surfaces a namespacedaiecsjs:error (with the original parse error attached ascause), consistent with the rest of the serializer. All existing length/bounds checks are unchanged.DeltaSerializer.apply()is now sound on a non-pristine / churned replica. It previously treated the wireeid(a raw slot index, generation 0) as a packed id — throwingaddComponent on dead entityonce a replica slot’s generation had advanced — and padded withwhile (targetState.size < e.eid) createEntity(), conflating the live count with a slot index and spawning phantom entities for holes in the source id space.apply()now materialises each entity at the source’s slot via a new internalensureEntityAtSlotprimitive (reuse a live slot, reclaim a freed one, or advance the frontier), using the slot’s current generation. No public API or wire-format change. Caveat: deltas carry only added/changed entities — entity/component removals are not propagated, soapply()remains additive.
[0.5.0] - 2026-05-30
Section titled “[0.5.0] - 2026-05-30”ai*js family version-unify milestone — the seven packages align on a common 0.5.0. No runtime API change; dist/ is byte-identical to 0.4.1 apart from the bumped VERSION string.
Changed
Section titled “Changed”- Migration-guide links repointed to GitHub blob URLs.
docs/became repository-only in 0.4.1 (dropped from the npmfiles[]), so the relative./docs/MIGRATION*.mdlinks inREADME.md/README_ZHTW.mdno longer resolved from the npm package page or the installed tarball. They now point athttps://github.com/islumina/aiecsjs/blob/main/docs/…so consumers can follow them. - Version aligned to the ai*js family
0.5.0unify milestone. A coordinated family-wide minor bump; aiecsjs carries no source / public-API / relations change in this release.
[0.4.1] - 2026-05-29
Section titled “[0.4.1] - 2026-05-29”Consistency patch — packaging and documentation surface aligned to the ai*js family. No runtime API change; dist/ is byte-identical to 0.4.0 apart from the bumped VERSION string.
Changed
Section titled “Changed”package.jsonpackaging metadata aligned to family conventions:engines.node">=18"→">=18.0.0";repository.urlgains thegit+prefix (git+https://github.com/islumina/aiecsjs.git). Both are semantically equivalent — registry/tooling hygiene only.files[]trimmed to the family-minimal set plusapi.json: the npm tarball now shipsdist,README.md,README_ZHTW.md,LICENSE,llms.txt,llms-full.txt, andapi.json.LICENSEis now listed explicitly (it was already published via npm’s automatic root-LICENSE inclusion).STABILITY.md,CHANGELOG.md, anddocs/are no longer bundled — they remain in the repository and stay reachable from the README/llms.txtlinks on GitHub.api.jsonis deliberately retained: it is the machine-readable export manifest (stability +sinceper entry) that this package’s “AI-readable docs” contract advertises, so it remains the tarball’s stability surface for tooling.
Removed
Section titled “Removed”- Redundant Traditional-Chinese doc duplicates:
STABILITY_ZHTW.mdandCHANGELOG_ZHTW.mdremoved. The family keepsREADME_ZHTW.mdas the single Traditional-Chinese entry point; per-export stability and the changelog are English-canonical (withapi.jsoncarrying the machine-readable stability surface). The plain-pipe language-switcher line atopSTABILITY.md/CHANGELOG.mdand the now-dangling_ZHTWreferences insideREADME_ZHTW.mdwere removed accordingly.
[0.4.0] - 2026-05-29
Section titled “[0.4.0] - 2026-05-29”getRelationData(world, source, rel, target): new stable export onaiecsjs/relations. Returns thedatapayload attached viaaddRelation, orundefinedwhen no such edge exists or no data was stored. Closes the write-only-data asymmetry present since 0.1:addRelationaccepted a data argument but there was no corresponding public read path.
Changed
Section titled “Changed”aiecsjs/relationsgraduated from experimental to stable. The graph API (defineRelation,addRelation,removeRelation,getRelationTargets,getRelationData) and the built-inChildOfrelation are now frozen for the 1.x track. SeeSTABILITY.mdfor the full stability contract, including the raw slot-keying ABA semantic.aiecsjs/workerremains experimental. True SAB shared-memory column aliasing is deferred; the worker sub-path continues on snapshot-copy semantics.
Build & Tooling
Section titled “Build & Tooling”- size-limit →
scripts/check-size.mjs: replaced thesize-limit+@size-limit/filedev dependencies with a zero-dependency script that measures transitive chunk-closure gzip size per ESM entry. Required becausetsup splitting: true(introduced in 0.3.1) makes each entry a thin re-export shell; the vanilla single-file measurement reported ~899 B for index when the true closure is ~7295 B. The new script resolves chunk imports recursively via BFS, sums per-file gzip, and enforces per-entry budgets. - npm → pnpm: migrated from
package-lock.jsontopnpm-lock.yaml. Added"packageManager": "pnpm@9.12.3"and"publishConfig": { "access": "public" }. CI and publish workflows updated to usepnpm/action-setup@v6+pnpm install --frozen-lockfile.npm publish --provenance --access publicin the publish workflow is intentionally preserved (OIDC trusted publishing requires npm CLI, not pnpm publish). - Coverage tests added + unreachable gaps documented: new tests cover previously-unreachable paths in
serialize.ts,component.ts,query.ts, andloop.ts. Thresholds updated to the honestly-achieved floor (statements 95 / branches 81 / functions 98 / lines 99). Unreachable-by-design gaps are now documented invitest.config.tswith Chesterton rationale.
[0.3.1] - 2026-05-29
Section titled “[0.3.1] - 2026-05-29”- Packed EntityId signed-overflow for generation ≥ 128:
createEntityreturned a negative number diverging from the unsigned value stored in archetype row arrays (Uint32Array), so query iteration (runQuery/iterQuery/forEachEntity) yielded an eid that failedentityRowlookups;refOf/entityExists/derefon a query-iterated high-generation entity misbehaved (refOfthrew on a live entity).packEid/packEntitynow normalise with>>> 0. No public-bundle behaviour change beyond the corrected eid representation (EntityId is opaque + in-memory-only). toJSONsilently dropped high-generation entities (gen ≥ 128 with default 8-bit generation):toJSONcontained its own inline pack expression that produced a signed (negative) result, diverging from the unsigned key stored inarch.entityRow. The affected entity passed the archetype check but failedentityRow.has(), so it was omitted from every snapshot andserializeWorldcall. Fixed by replacing the inline expression with the canonicalpackEid(which applies>>> 0). SPOT principle: one pack source of truth.- Cross-subpath registry isolation (
tsup splitting: false→splitting: true): each compiled entry point (dist/index.js,dist/serialize.js, etc.) previously bundled its own private copy ofinternal/world.ts, including the module-scopeworldRegistry. A world created via the core subpath was invisible toserializeWorld/getRelationTargets/transferableSnapshotimported from their respective subpaths, causingworld N is destroyed or unknownat runtime. Withsplitting: true, esbuild extracts a shared chunk used by all entries; ESM and CJS are both verified by the newscripts/check-dist-subpaths.mjssmoke script. getRelationTargetsreturned raw index asEntityId(gen always 0):addRelationstores the target as a raw slot index (& indexMask). The previous return path cast this raw index directly toEntityId, which is equivalent to a packed id with generation 0. For any target that had been recycled (gen > 0), callers received a stale id that failedentityExists,entityRowlookups, and component access. Fixed by re-packing each raw index against the current generation viapackEidbefore returning.resolveOptionsdid not validateindexBits + generationBits ≤ 32: the individual range checks (indexBits ∈ [1, 24],generationBits ∈ [0, 16]) allowed combinations such asindexBits=24, generationBits=16(40 bits), wheregen << 24silently overflowed and high-generation bits were lost. A sum check is now enforced with a clear error message. The[Unreleased]example corrected accordingly (indexBits: 16, generationBits: 16= 32 bits).
Known Limitations
Section titled “Known Limitations”createDeltaSerializer.applywith a recycled target world:applyuses the raw entity index from the delta snapshot as theEntityIddirectly. When the target world has already recycled any of those slots (generation > 0), component operations silently act on the wrong packed id. This is a known limitation of the experimental delta API; the common usage (delta → a fresh gen-0 render-mirror world) is unaffected. A proper raw-index-to-packed-id mapping is planned for 0.4. Avoidapplyagainst a world that has previously destroyed entities.
Documentation
Section titled “Documentation”- README / README_ZHTW updated to reflect the shipped 0.3.0
EntityRefAPI: the previous README still described EntityRef as “targeted for 0.3+” andgetEntityGeneration/packEntityas experimental. Both files now correctly state EntityId has been packed since 0.3, andEntityRef/refOf/deref/aliveRef/EntityNotAliveErrorare all stable since 0.3.0. API table entries for these symbols added.
Build & Tooling
Section titled “Build & Tooling”- Coverage gate:
@vitest/coverage-v8installed and wired intoprepublishOnly(replacesnpm run test) and CI. Thresholds: statements 95 / branches 80 / functions 97 / lines 98 — the achievable bar on pristine source. The branch figure honours the?? 0/noUncheckedIndexedAccessidiom on TypedArray reads (nullish-fallback branches unreachable by design); thresholds are raised only by adding tests, never by stripping defensive guards or scattering/* v8 ignore */. fast-checkproperty tests (tests/properties.test.ts): pack/unpack round-trip invariant (assertse >= 0to guard the P0 regression) and ABA-deref always-null invariant.- Dispose three-cycle tests, error-path tests, and observer handler-throw behaviour documented in
tests/world.test.ts/tests/observers.test.ts. scripts/check-dist-subpaths.mjs(npm run verify:dist): post-build smoke test that importscreateWorld+createEntityfrom the core subpath and callsserializeWorld,addRelation/getRelationTargets, andtransferableSnapshotfrom their respective subpaths for both ESM (dist/*.js) and CJS (dist/*.cjs). Wired intoprepublishOnly(afterbuild) and CI.
[0.3.0] - 2026-05-29
Section titled “[0.3.0] - 2026-05-29”Added (API)
Section titled “Added (API)”EntityRef<T>— ABA-safe entity reference.refOf(world, eid)builds one;deref(world, ref)returns the entity id when still alive (generation match) ornullotherwise;aliveRef(world, ref)is the boolean guard form. Phantom typeTlets callers distinguish ref kinds (e.g.EntityRef<'bullet'>) without runtime cost. Refs are in-memory only — not serializable across worker / disk.EntityNotAliveError— thrown byrefOfwhen the entity is dead or invalid.deref/aliveRefnever throw.
Changed
Section titled “Changed”EntityIdnow packs index + generation into a single 32-bit number(generation << indexBits) | index(defaultindexBits=24, generationBits=8).EntityIdremains opaque per STABILITY contract; the layout is implementation detail. Migration note: do not compareEntityIdnumbers directly (eid === 42will break across slot recycles); usegetEntityIndex(eid)for index comparison orrefOf(world, eid).idfor identity matching that survives slot reuse.getEntityGeneration/packEntitygraduate tostable(wereexperimentalsince 0.2.0). Both now return real values. These functions use default 24/8 bit layout; for non-defaultcreateWorld({ indexBits, generationBits }), useEntityRefandderefinstead of manual unpacking.
- ABA bug on entity slot recycle: previously
entityExistsandisAliveInternalonly checked archetype membership; a staleEntityIdpointing at a recycled slot would silently report alive. With packed generation +derefgeneration match, stale refs now correctly invalidate. destroyEntitygeneration wrap mask aligned withoptions.generationBits(was hard-coded& 0xffff). The mask now correctly usesstate.options.generationMask, fixing inconsistency for non-defaultgenerationBitsvalues.
Documentation
Section titled “Documentation”onSetJSDoc clarifies thataddComponentdoes NOT triggeronSet, and direct writes to column views returned bygetComponent(e.g.col.x[idx] = 5) also do NOT triggeronSet. OnlysetComponenton an already-present component fires the callback. Anti-pattern example included.
Compatibility
Section titled “Compatibility”EntityIdlayout change is not breaking at the type system level (opaque branded number), but consumers who relied oneid === Ndirect comparison will need to migrate (see Migration note above).- All existing
stableexports unchanged. aiecsjs/workersnapshot wire format unchanged (still uses raw indices).aiecsjs/serializewire format unchanged.
Build & tooling
Section titled “Build & tooling”VERSIONconstant bumped to0.3.0.
[0.2.1] - 2026-05-28
Section titled “[0.2.1] - 2026-05-28”Security
Section titled “Security”- Resolve two Dependabot moderate advisories on the transitive dev-only graph by upgrading
vitest1.6.0 → 4.1.7. Addsvite8.0.14 as a direct devDependency to satisfy vitest 4’s peer range (^6 || ^7 || ^8). These are dev-only — runtime surface unchanged.- GHSA-67mh-4wv8-2f99
esbuild <=0.24.2CORS development server data leak (fixed in 0.25.0). - GHSA-4w7w-66w2-5vf9
vite <=6.4.1path traversal in optimized deps.maphandling (fixed in 6.4.2 / 7.3.2 / 8.0.5).
- GHSA-67mh-4wv8-2f99
Changed
Section titled “Changed”- README opening unified across the ai*js family: five-badge shields row (npm + CI + License + AI Generated + 繁體中文/English), one-line tagline as blockquote, ecosystem footer linking to the other two packages. Replaces the previous mixed style (text language switcher + 5 ad-hoc badges).
VERSIONconstant bumped to 0.2.1 (src/version.ts) soworld.versionand snapshot meta reflect this release.
Runtime surface unchanged. Production bundles are byte-identical to 0.2.0.
[0.2.0] - 2026-05-28
Section titled “[0.2.0] - 2026-05-28”Fixed (correctness + security)
Section titled “Fixed (correctness + security)”- Prototype-pollution hardening in AoS
writeInitial(src/internal/component.ts): replacedObject.assign(inst, initial)with an explicit own-key copy that filters__proto__/constructor/prototype. Closes a path where a maliciousJSON.parsepayload reachingaddComponent/setComponent/fromJSON/deserializeWorldcould clobber the per-instance prototype. - Observer dispatch is now safe against unsubscribe-during-iteration (src/observers.ts): every
fire*walks a snapshot ofstate.observers(Array.from(...)+includesguard) so a handler that calls its own returned disposer no longer skips sibling observers in the same fire round. removeComponentwrites the new entity mask BEFORE firing observers (src/internal/component.ts): query-targetedremoveobservers readstate.entityMaskto decide if the entity left the matching set; with the previous ordering the bit was still set during dispatch and the remove never fired. BringsremoveComponentin line withaddComponent’s “mutate then fire” order.destroyEntitynow emits query-targetedremoveevents (src/observers.tsdispatchDestroyObservers): in addition to per-componentonRemove, the destroy hook now walks query observers and firesremovefor any query the entity was matching pre-destroy.wasMatchis computed against a snapshot of the pre-destroy mask (not livestate.entityMask) so a Phase 1 reentrant handler that mutates the entity’s mask cannot suppress query removes in Phase 2 (regression caught by the round-2 review).deserializeWorld/attachWorld/adoptSnapshotbinary length fields are bounds-checked (src/serialize.ts):verLenandjsonLencarry explicitoff + len <= bytes.lengthassertions and a 64 MiB cap.attachWorldandadoptSnapshot(src/worker.ts) both carry SECURITY JSDocs that document the trust boundary expectation for SAB / TransferableSnapshot transports.
Added (API)
Section titled “Added (API)”disposeWorld(world)— new export that aliasesdestroyWorld. Aligns with the ai*js ecosystemdispose()convention (aifsmjs.Runtime.dispose,aibridgejs.Bridge.dispose). Prefer this name in new code;destroyWorldis retained as a deprecated alias and is scheduled for removal in 1.0.{ signal?: AbortSignal }on every observer:onAdd,onRemove,onSet, andobservenow accept an options object. When the signal aborts, the observer auto-unsubscribes. The returned unsubscribe function remains valid and idempotent. New exported typeObserverOptionsdocuments the shape. This closes a long-running gap noted in the AI ecosystem audit — long-lived observers on user-controlled lifecycles (UI components, async pipelines) no longer require manual cleanup wiring.
Changed (stability)
Section titled “Changed (stability)”getEntityGenerationandpackEntityre-classified fromstable→experimentalinSTABILITY.mdandapi.json. In 0.1 these returned0/ identity and that has not changed — the relabel honestly admits the deferred encoding work. Real values arrive when ABA-safeEntityReflands.destroyWorldre-classified fromstable→deprecated. Behaviour unchanged; the deprecation is the API-naming alignment described above. UsedisposeWorldinstead.
Documentation
Section titled “Documentation”onSetnow carries a JSDoc and README paragraph clarifying that it is a low-level mutation hook, not a reactive value-predicate query.enterQuery/exitQuerycontinue to be the structural-change surface; reactive value tracking remains an explicit non-goal of the core.- README observer section gains an
AbortController-based unsubscribe example.
Build & tooling
Section titled “Build & tooling”- Added Biome lint + format (
biome.json,npm run lint,npm run format). Brings parity withaifsmjsandaibridgejsand surfacesnoExplicitAnywarnings in legacysrc/internal/*for follow-up cleanup. - Added
scripts/verify-exports.mjsand thenpm run verify:exportsscript; gates that everypackage.json#exportsentry has a real file indist/. Wired intoprepublishOnly. - New
CONTRIBUTING.mdwith the same shape used byaifsmjs(quick start, scope policy, release flow).
Compatibility
Section titled “Compatibility”This release is non-breaking at runtime. All existing code that called destroyWorld(world), registered observers without options, or read getEntityGeneration continues to work. The stability label change is documentation-only.
[0.1.4] - 2026-05-28
Section titled “[0.1.4] - 2026-05-28”Docs-only release. Adds a cross-package integration section pointing at the aibridgejs JSON envelope contract; no source code changes.
Documentation
Section titled “Documentation”- README and README_ZHTW gained an “Integration with aibridgejs” section explaining that
bridge.call/bridge.emitenforce JSON-safe payloads and silently dropDate,Map,Set, and class instances. The correct shape for streaming world state across the bridge istoJSON(world)(orserializeWorld(world)wrapped in a JSON envelope) before emitting, notgetComponent(...)direct. See aiecsjs README · Integration with aibridgejs. - Verified via the
aijs-integration-smokecompanion project: every named export fromaifsmjs@0.1.2,aibridgejs@0.1.3, andaiecsjs@0.1.3can coexist in a single TypeScript module with zero identifier collisions undertsc --noEmit --strict.
[0.1.3] - 2026-05-28
Section titled “[0.1.3] - 2026-05-28”A “no known silent bugs” release. Two correctness fixes, one hot-path allocation removal, and a small batch of style cleanups. No public API behaviour changes; _getWorldState is removed from the root export (was undocumented, unused by every sub-path, leading underscore signalled internal).
aiecsjs/relationsrelation data store no longer keys edges bysrcEid * worldCapacity + tgtEid. After the world grew, the same(src, tgt)pair computed a different key and earlier entries became orphaned. Storage is now a nestedMap<srcEid, Map<tgtEid, data>>, independent of capacity. The cleanup hook ondestroyEntitywas updated to match. v0.1 has no public retrieve API so the bug was user-invisible, but it would have surfaced the moment a retrieve surface landed in 0.2.- Per-world resolved query bitmasks no longer live on the module-global
QueryInternal. When the samedefineQuery(...)handle was used by two worlds whose component registration orders differed, the second world’s per-world mask overwrote the first world’s, andrunQuerysilently returned wrong rows in world A. Masks now live inWorldState.queryMasks: Map<queryId, QueryMaskBundle>, isolated per world. Regression test intests/multi-world.test.tsexercises the cross-order scenario.
Changed (internal)
Section titled “Changed (internal)”- Observer dispatch (
dispatchQueryObservers) no longer allocates a temporaryUint32Arrayon every mutation event. AddedmatchesEntityMaskhelper inbitmask.tsthat reads directly fromstate.entityMaskat a base offset. - Shared bit-iteration extracted as
forEachSetBit(mask, base, words, fn)inbitmask.ts.clearAllEntityStorages(component.ts) anddispatchDestroyObservers(observers.ts) now share that single implementation instead of inlining the sameword & -word/Math.clz32pattern three times. state.generations[idx]is written without anas anycast.Uint8Array | Uint16Arrayalready supports indexed read/write.- Removed the
void oldCapno-op fromgrowEntityArrays. - Removed
_getWorldStatefrom the rootaiecsjsexport. Sub-paths (aiecsjs/serialize,aiecsjs/worker) already importgetWorldStatedirectly from the internal module; the leading-underscore root re-export had no consumer.
Planned for 0.3
Section titled “Planned for 0.3”- Promote
aiecsjs/relationsandaiecsjs/workertostable. - Stabilize the network delta wire format.
- Add automated benchmark suite committed to repo.
Planned for 1.0
Section titled “Planned for 1.0”- API freeze for the 1.x line.
- Drop the experimental status label.
[0.1.2] - 2026-05-28
Section titled “[0.1.2] - 2026-05-28”CI/CD smoke-test release. No user-facing source or behavioural changes since 0.1.1; this bump exists solely to validate the tag-triggered publish workflow (see .github/workflows/publish.yml) end-to-end against the npm registry with provenance attestation.
Build & tooling
Section titled “Build & tooling”- Confirmed that pushing a
v*.*.*tag triggers.github/workflows/publish.yml, runsprepublishOnly(typecheck + tests + build + size budget), and publishes to npm with sigstore provenance.
[0.1.1] - 2026-05-28
Section titled “[0.1.1] - 2026-05-28”The “documentation honesty + test backstop” release. No new public APIs; this is the version of 0.1.0 that ships with the public surface, the documentation, and the test coverage in agreement.
destroyEntitynow clears the SoA columns and undefines the AoS slots that the destroyed entity owned. Previously only the entity mask was cleared, leaving stale data at the slot visible to debug snapshots and the serialisation path. PublichasComponent/ query behaviour was already correct, so user-visible behaviour is unchanged; this closes the gap surfaced by the newdestroyEntity zeroes the destroyed entity's SoA slottest.
Changed (docs hygiene)
Section titled “Changed (docs hygiene)”- README and STABILITY now describe
aiecsjs/workerhonestly as a snapshot-copy transport for 0.1; true shared columns remain a 0.2 target. README description andpackage.jsondescription updated accordingly. - README clarifies that 0.1
EntityIdis a bare slot index; internal generation is tracked for slot reuse but not encoded in the ID. ABA-safeEntityRefis on the 0.2 roadmap. - Sub-paths (
loop/commands/observers/serialize/worker/relations) re-positioned in STABILITY as utility / adapter sub-paths; the rootaiecsjsis the stable core surface. Tree-shakers should drop any sub-path the app does not import. - README adds a “What aiecsjs does NOT do” section listing explicit non-goals (system scheduler, render binding, physics, network replication, value-predicate reactive queries, prefab/inheritance).
- Language version filenames renamed from
*.zh-TW.mdto*_ZHTW.md. Cross-links,llms.txt, andpackage.jsonfilesupdated. Future language variants follow the same uppercase ISO 639-1 pattern. - Removed emoji from documentation prose (language switchers, status banners).
Build & tooling
Section titled “Build & tooling”- tsup build now runs with
minify: true. size-limitadded as a dev dependency; per-export gzip budgets enforced vianpm run size. Current measurements: core 5.49 kB, all sub-paths combined 12.6 kB gzip.- GitHub Actions CI workflow added: typecheck → test → build → size check on push and PR to
main. prepublishOnlynow runs typecheck, tests, build, and the size budget gate before allowing publish.
- Test count increased from 84 to 140. New file
tests/internal/bitmask.test.tscovers the multi-word bitmask helpers in isolation (27 cases includingmatchestruth table). New filetests/multi-world.test.tscovers per-world isolation when the same component is reused. Existing files gained: naive linear-filter cross-check againstrunQueryfor all clause combinations, archetype migration boundary path, query mid-traversal stability and lazy cache behaviour, SoA field clear assertions on bothremoveComponentanddestroyEntity, SoA vector-length round trip,maxEntities/maxComponentsboundary throws, observer fan-out for destroy across multiple components,onSetvalue content, query observer ignores unrelated mutation, relation source-side destroy cleanup, exclusive relation storage resize, workerreadOnlyrejects add / remove / destroy, serializeoptions.componentsfilter,onUnknownVersion: throw | best-effortpaths, command buffer placeholder resolves into a queryable entity, slot-reuse limitation made explicit. Loop tests rewritten on top ofvi.useFakeTimers({ toFake: ['performance', ...] })for deterministic dt validation.
0.1.0 - 2026-05-27
Section titled “0.1.0 - 2026-05-27”Initial release. All 50 documented exports across 7 modules are implemented and covered by 84 passing Vitest behaviour tests. Built with tsup to dual ESM + CJS, ships .d.ts declarations and source maps.
Implementation notes
Section titled “Implementation notes”- Storage: world-level TypedArray columns per SoA component field, sized to world capacity. Archetypes track entity membership (a
Uint32Array entities[]) but do not own column data. This makes archetype migration O(1) and lets columns be indexed by the entity index (getEntityIndex(eid)) directly, without per-archetype indirection. (In 0.1EntityIdwas the index, soPosition.x[eid]worked literally; since 0.3 packs a generation into the id, hot-loop code must index withPosition.x[getEntityIndex(eid)]— the raw packed id is no longer the column offset.) Trade-off: iteration over archetypes reads columns at potentially non-contiguous offsets; for hot data this stays in L1. - EntityId is unversioned in 0.1:
EntityIdis the entity index. Generation is tracked internally for slot reuse but not encoded in the ID.getEntityIndex/getEntityGeneration/packEntityare identity helpers. ABA-safe references via a separateEntityReftype are planned for 0.2. - Bitmask queries: multi-word Uint32 masks, default 8 words (256 components). Per-world bit allocation, global component identity.
- Worker / SAB: 0.1 implements snapshot-copy semantics (serialize-into-SAB on send, deserialize-on-adopt) rather than true shared-memory column aliasing. The API surface matches the documented contract; true shared columns ship in 0.2.
- Binary serialization: a JSON payload wrapped in a 4-byte magic + version header. Compact binary column encoding is planned for 0.2.
README.md(English) andREADME_ZHTW.md(Traditional Chinese) with quick start, guide, API reference, performance notes, multi-threading guide, WebGPU interop section, serialization guide, migration guides, and “For AI Agents” section.llms.txt— Jeremy Howard format AI-discovery file.llms-full.txt— Single-file complete reference for LLM consumption.api.json— Machine-readable export manifest with stability andsincefields on every entry.STABILITY.mdandSTABILITY_ZHTW.md— Per-export stability contract.docs/MIGRATION.mdanddocs/MIGRATION_ZHTW.md— Migration guides from bitECS 0.4, miniplex 2.0, and ECSY.
API surface declared
Section titled “API surface declared”- Core:
createWorld,destroyWorld,resetWorld,getWorldSize,getWorldCapacity. - Entity:
createEntity,destroyEntity,entityExists,getEntityIndex,getEntityGeneration,packEntity. - Component:
defineComponent,defineTag,defineObjectComponent,addComponent,removeComponent,hasComponent,getComponent,setComponent,Types. - Query:
defineQuery,runQuery,forEachEntity,iterQuery,enterQuery,exitQuery,queryArchetypes(experimental). - System:
pipe. - Subpath
aiecsjs/loop:createLoop. - Subpath
aiecsjs/commands:createCommandBuffer,flush,withCommandBuffer. - Subpath
aiecsjs/observers:observe,onAdd,onRemove,onSet. - Subpath
aiecsjs/serialize:serializeWorld,deserializeWorld,toJSON,fromJSON,createDeltaSerializer(experimental). - Subpath
aiecsjs/worker(experimental):transferableSnapshot,adoptSnapshot,attachWorld,detachWorld. - Subpath
aiecsjs/relations(experimental, not implemented):defineRelation,addRelation,removeRelation,getRelationTargets,ChildOf. - Utility:
VERSION,IS_SAB_SUPPORTED,isWorld,isEntity.
Known limitations in 0.1
Section titled “Known limitations in 0.1”aiecsjs/relationsandaiecsjs/workerare implemented but tagged experimental; API may shift.- Network delta wire format is JSON-based; binary patch format is planned for 0.2.
- AoS components are main-thread only; cannot be shared via SharedArrayBuffer.
- No automatic system scheduler / parallel execution.
- Worker/SAB uses snapshot-copy in 0.1 rather than true shared-memory aliasing.
- EntityId is unversioned; ABA-safe references arrive with
EntityRefin 0.2.