aiecsjs 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +20 -11
  3. package/README_ZHTW.md +17 -9
  4. package/STABILITY.md +11 -9
  5. package/STABILITY_ZHTW.md +3 -3
  6. package/api.json +52 -24
  7. package/dist/commands.cjs +1 -1
  8. package/dist/commands.cjs.map +1 -1
  9. package/dist/commands.d.cts +1 -1
  10. package/dist/commands.d.ts +1 -1
  11. package/dist/commands.js +1 -1
  12. package/dist/commands.js.map +1 -1
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +4 -4
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/observers.cjs +1 -1
  20. package/dist/observers.cjs.map +1 -1
  21. package/dist/observers.d.cts +23 -6
  22. package/dist/observers.d.ts +23 -6
  23. package/dist/observers.js +1 -1
  24. package/dist/observers.js.map +1 -1
  25. package/dist/relations.cjs.map +1 -1
  26. package/dist/relations.d.cts +1 -1
  27. package/dist/relations.d.ts +1 -1
  28. package/dist/relations.js.map +1 -1
  29. package/dist/serialize.cjs +1 -1
  30. package/dist/serialize.cjs.map +1 -1
  31. package/dist/serialize.d.cts +1 -1
  32. package/dist/serialize.d.ts +1 -1
  33. package/dist/serialize.js +1 -1
  34. package/dist/serialize.js.map +1 -1
  35. package/dist/{types-B3YUJZEg.d.cts → types-BGEeHad-.d.cts} +1 -1
  36. package/dist/{types-B3YUJZEg.d.ts → types-BGEeHad-.d.ts} +1 -1
  37. package/dist/worker.cjs +1 -1
  38. package/dist/worker.cjs.map +1 -1
  39. package/dist/worker.d.cts +23 -1
  40. package/dist/worker.d.ts +23 -1
  41. package/dist/worker.js +1 -1
  42. package/dist/worker.js.map +1 -1
  43. package/llms-full.txt +1148 -373
  44. package/package.json +8 -2
package/CHANGELOG.md CHANGED
@@ -8,11 +8,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
- ### Planned for 0.2
11
+ ### Planned for 0.3+
12
12
 
13
- - Implement `aiecsjs/relations`: `defineRelation`, `addRelation`, `removeRelation`, `getRelationTargets`, `ChildOf`.
13
+ - Implement ABA-safe `EntityRef` and graduate `getEntityGeneration` / `packEntity` from experimental → stable.
14
14
  - Add `pipeAsync` for async system composition.
15
15
  - Doc-test harness so README code blocks are mechanically verified.
16
+ - Promote `aiecsjs/relations` and `aiecsjs/worker` (true SAB-shared columns) to `stable`.
17
+
18
+ ## [0.2.0] - 2026-05-28
19
+
20
+ ### Fixed (correctness + security)
21
+
22
+ - **Prototype-pollution hardening in AoS `writeInitial`** ([src/internal/component.ts](src/internal/component.ts)): replaced `Object.assign(inst, initial)` with an explicit own-key copy that filters `__proto__` / `constructor` / `prototype`. Closes a path where a malicious `JSON.parse` payload reaching `addComponent` / `setComponent` / `fromJSON` / `deserializeWorld` could clobber the per-instance prototype.
23
+ - **Observer dispatch is now safe against unsubscribe-during-iteration** ([src/observers.ts](src/observers.ts)): every `fire*` walks a snapshot of `state.observers` (`Array.from(...)` + `includes` guard) so a handler that calls its own returned disposer no longer skips sibling observers in the same fire round.
24
+ - **`removeComponent` writes the new entity mask BEFORE firing observers** ([src/internal/component.ts](src/internal/component.ts)): query-targeted `remove` observers read `state.entityMask` to decide if the entity left the matching set; with the previous ordering the bit was still set during dispatch and the remove never fired. Brings `removeComponent` in line with `addComponent`'s "mutate then fire" order.
25
+ - **`destroyEntity` now emits query-targeted `remove` events** ([src/observers.ts](src/observers.ts) `dispatchDestroyObservers`): in addition to per-component `onRemove`, the destroy hook now walks query observers and fires `remove` for any query the entity was matching pre-destroy. `wasMatch` is computed against a **snapshot of the pre-destroy mask** (not live `state.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).
26
+ - **`deserializeWorld` / `attachWorld` / `adoptSnapshot` binary length fields are bounds-checked** ([src/serialize.ts](src/serialize.ts)): `verLen` and `jsonLen` carry explicit `off + len <= bytes.length` assertions and a 64 MiB cap. `attachWorld` and `adoptSnapshot` ([src/worker.ts](src/worker.ts)) both carry SECURITY JSDocs that document the trust boundary expectation for SAB / TransferableSnapshot transports.
27
+
28
+ ### Added (API)
29
+
30
+ - **`disposeWorld(world)`** — new export that aliases `destroyWorld`. Aligns with the ai*js ecosystem `dispose()` convention (`aifsmjs.Runtime.dispose`, `aibridgejs.Bridge.dispose`). Prefer this name in new code; `destroyWorld` is retained as a deprecated alias and is scheduled for removal in 1.0.
31
+ - **`{ signal?: AbortSignal }` on every observer**: `onAdd`, `onRemove`, `onSet`, and `observe` now accept an options object. When the signal aborts, the observer auto-unsubscribes. The returned unsubscribe function remains valid and idempotent. New exported type `ObserverOptions` documents 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.
32
+
33
+ ### Changed (stability)
34
+
35
+ - `getEntityGeneration` and `packEntity` re-classified from `stable` → `experimental` in `STABILITY.md` and `api.json`. In 0.1 these returned `0` / identity and that has not changed — the relabel honestly admits the deferred encoding work. Real values arrive when ABA-safe `EntityRef` lands.
36
+ - `destroyWorld` re-classified from `stable` → `deprecated`. Behaviour unchanged; the deprecation is the API-naming alignment described above. Use `disposeWorld` instead.
37
+
38
+ ### Documentation
39
+
40
+ - `onSet` now carries a JSDoc and README paragraph clarifying that it is a **low-level mutation hook**, not a reactive value-predicate query. `enterQuery` / `exitQuery` continue to be the structural-change surface; reactive value tracking remains an explicit non-goal of the core.
41
+ - README observer section gains an `AbortController`-based unsubscribe example.
42
+
43
+ ### Build & tooling
44
+
45
+ - Added [Biome](https://biomejs.dev/) lint + format (`biome.json`, `npm run lint`, `npm run format`). Brings parity with `aifsmjs` and `aibridgejs` and surfaces `noExplicitAny` warnings in legacy `src/internal/*` for follow-up cleanup.
46
+ - Added `scripts/verify-exports.mjs` and the `npm run verify:exports` script; gates that every `package.json#exports` entry has a real file in `dist/`. Wired into `prepublishOnly`.
47
+ - New `CONTRIBUTING.md` with the same shape used by `aifsmjs` (quick start, scope policy, release flow).
48
+
49
+ ### Compatibility
50
+
51
+ 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.
16
52
 
17
53
  ## [0.1.4] - 2026-05-28
18
54
 
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  > A TypeScript-first archetype ECS for browser and Node, with SAB-ready snapshot transport and AI-readable documentation.
12
12
 
13
- aiecsjs uses **archetype tables with TypedArray columns** and **bitmask queries** — the same architecture that powers piecs and wolf-ecs at the top of public benchmarks. Its API is **functional and tree-shakable**, composed with `pipe()`. Components support both Structure-of-Arrays (SoA) and Array-of-Structures (AoS) layouts. Entity IDs in 0.1 are bare slot indices; internal generation tracks slot reuse but is not encoded in the ID. ABA-safe `EntityRef` ships in 0.2.
13
+ aiecsjs uses **archetype tables with TypedArray columns** and **bitmask queries** — the same architecture that powers piecs and wolf-ecs at the top of public benchmarks. Its API is **functional and tree-shakable**, composed with `pipe()`. Components support both Structure-of-Arrays (SoA) and Array-of-Structures (AoS) layouts. Entity IDs in 0.x are bare slot indices; internal generation tracks slot reuse but is not encoded in the ID. ABA-safe `EntityRef` is targeted for **0.3+** (deferred from the 0.2 roadmap once the v0.2.0 scope froze on API stability + safety).
14
14
 
15
15
  ```ts
16
16
  import { createWorld, createEntity, defineComponent, defineQuery, pipe, forEachEntity, Types } from 'aiecsjs'
@@ -64,7 +64,7 @@ pipe(movement)(world, 1/60)
64
64
  | Storage | Archetype + SoA columns | SparseSet + bitmask + SoA/AoS | Archetype + JS objects | Configurable (packed/sparse/compact) + ArrayBuffer |
65
65
  | API style | Functional + `pipe` | Functional + `pipe` | Chainable OO | Decorator classes |
66
66
  | TS inference on query | Tuple-aware columns | Manual | Predicate inference | Class-based |
67
- | Multi-thread | SAB snapshot transport (0.1); true shared cols planned 0.2 | SAB-ready, scheduling DIY | Single-thread | Roadmap (not shipped) |
67
+ | Multi-thread | SAB snapshot transport (0.x); true shared cols planned 0.3+ | SAB-ready, scheduling DIY | Single-thread | Roadmap (not shipped) |
68
68
  | AI docs | `llms.txt` + `llms-full.txt` + `api.json` | No | No | No |
69
69
  | Maintenance | Active (new) | Active | Slowed (~3y since npm release) | Active |
70
70
 
@@ -285,7 +285,13 @@ const stopAdd = onAdd(world, Position, (e) => console.log('positioned', e))
285
285
  const stopRemove = onRemove(world, Player, (e) => console.log('un-playered', e))
286
286
  const stopSet = onSet(world, Health, (e, val) => console.log('health set', e, val))
287
287
 
288
- // later
288
+ // Auto-unsubscribe via AbortSignal (since 0.2.0):
289
+ const ac = new AbortController()
290
+ onAdd(world, Position, (e) => trackEntity(e), { signal: ac.signal })
291
+ // later, abort once and all observers attached to this signal are removed
292
+ ac.abort()
293
+
294
+ // Or the returned unsubscribe — both are idempotent and may be combined:
289
295
  stopAdd()
290
296
  stopRemove()
291
297
  stopSet()
@@ -293,6 +299,8 @@ stopSet()
293
299
 
294
300
  Observers fire synchronously inside the mutation call. Use them for side effects that must happen at the exact moment of the change (debugging, replication). For batched UI updates, prefer reactive queries.
295
301
 
302
+ **`onSet` is a low-level mutation hook**, not a reactive value-predicate query. It fires after `setComponent(world, eid, comp, value)` when the component is already present on the entity — `addComponent` does NOT trigger `onSet` (use `onAdd` for that path; an `addComponent` followed by `setComponent` fires both, in that order). `enterQuery` / `exitQuery` respond to structural component-set changes only; if you need a reactive "value crossed threshold" view, layer that in app code on top of `onSet`.
303
+
296
304
  ### Command buffers — when and why
297
305
 
298
306
  The golden rule: **do not add or remove components on entities you're currently iterating over.** Doing so can skip or double-process entities because the archetype membership changes mid-walk. Use a command buffer to defer:
@@ -335,7 +343,7 @@ addRelation(world, alice, ChildOf, parent)
335
343
  const parentOfAlice = getRelationTargets(world, alice, ChildOf)
336
344
  ```
337
345
 
338
- The 0.2 release adds exclusive relations (one target only), wildcard queries, and serialization of relation graphs.
346
+ Relation graph stabilisation — exclusive relations (one target only), wildcard queries, and serialisation of relation graphs — is targeted for **0.3+**; 0.2.0 keeps the relations subpath in its existing experimental shape (no breaking changes).
339
347
 
340
348
  ## API Reference
341
349
 
@@ -346,7 +354,8 @@ Full machine-readable surface in [`api.json`](./api.json). Stability flags in [`
346
354
  | Function | Signature | Stability |
347
355
  |---|---|---|
348
356
  | `createWorld` | `(options?: WorldOptions) => World` | stable |
349
- | `destroyWorld` | `(world: World) => void` | stable |
357
+ | `disposeWorld` | `(world: World) => void` | stable (since 0.2.0) |
358
+ | `destroyWorld` | `(world: World) => void` | **deprecated** since 0.2.0 — alias of `disposeWorld`; scheduled for removal in 1.0 |
350
359
  | `resetWorld` | `(world: World) => void` | stable |
351
360
  | `getWorldSize` | `(world: World) => number` (alive count) | stable |
352
361
  | `getWorldCapacity` | `(world: World) => number` | stable |
@@ -371,8 +380,8 @@ type WorldOptions = {
371
380
  | `destroyEntity` | `(world: World, eid: EntityId) => void` | stable |
372
381
  | `entityExists` | `(world: World, eid: EntityId) => boolean` | stable |
373
382
  | `getEntityIndex` | `(eid: EntityId) => number` | stable |
374
- | `getEntityGeneration` | `(eid: EntityId) => number` | stable |
375
- | `packEntity` | `(index: number, generation: number) => EntityId` | stable |
383
+ | `getEntityGeneration` | `(eid: EntityId) => number` | experimental (since 0.1) — returns `0` in 0.x; real generation lands with ABA-safe `EntityRef` in **0.3+** |
384
+ | `packEntity` | `(index: number, generation: number) => EntityId` | experimental (since 0.1) — identity in 0.x; real packing lands with `EntityRef` in **0.3+** |
376
385
 
377
386
  ### Component — `aiecsjs`
378
387
 
@@ -429,10 +438,10 @@ const Types = { i8, u8, i16, u16, i32, u32, f32, f64, eid, bool } as const
429
438
 
430
439
  | Function | Signature | Stability |
431
440
  |---|---|---|
432
- | `observe` | `(world, q, event, handler) => () => void` | stable |
433
- | `onAdd` | `(world, comp, handler) => () => void` | stable |
434
- | `onRemove` | `(world, comp, handler) => () => void` | stable |
435
- | `onSet` | `(world, comp, handler) => () => void` | stable |
441
+ | `observe` | `(world, q, event, handler, opts?: { signal? }) => () => void` | stable |
442
+ | `onAdd` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable |
443
+ | `onRemove` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable |
444
+ | `onSet` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable; low-level mutation hook, not reactive |
436
445
 
437
446
  ### Serialization — `aiecsjs/serialize`
438
447
 
package/README_ZHTW.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  > 為 TypeScript 而設計的原型式 ECS,支援瀏覽器與 Node,內建 SAB 快照傳輸(snapshot transport)與 AI 可讀文件。
12
12
 
13
- aiecsjs 採用 **原型表格搭配 TypedArray 欄位** 與 **位元遮罩查詢**,這正是 piecs 與 wolf-ecs 在公開效能評測中名列前茅所採用的架構。API 為 **函式式且可 tree-shake**,以 `pipe()` 組合。元件(Component)同時支援 SoA(結構陣列)與 AoS(結構物件)兩種佈局。0.1 的實體 ID 為純索引值;世代計數在內部追蹤槽位重用,但不編入 ID。具 ABA 安全的 `EntityRef` 預計 0.2 推出。
13
+ aiecsjs 採用 **原型表格搭配 TypedArray 欄位** 與 **位元遮罩查詢**,這正是 piecs 與 wolf-ecs 在公開效能評測中名列前茅所採用的架構。API 為 **函式式且可 tree-shake**,以 `pipe()` 組合。元件(Component)同時支援 SoA(結構陣列)與 AoS(結構物件)兩種佈局。0.x 的實體 ID 為純索引值;世代計數在內部追蹤槽位重用,但不編入 ID。具 ABA 安全的 `EntityRef` 預計 **0.3+** 推出(0.2 為求 API 穩定與安全性收尾而延後)。
14
14
 
15
15
  ```ts
16
16
  import { createWorld, createEntity, defineComponent, defineQuery, pipe, forEachEntity, Types } from 'aiecsjs'
@@ -64,7 +64,7 @@ pipe(movement)(world, 1/60)
64
64
  | 儲存模型 | 原型 + SoA 欄位 | SparseSet + bitmask + SoA/AoS | 原型 + JS 物件 | 可選(packed/sparse/compact)+ ArrayBuffer |
65
65
  | API 風格 | 函式式 + `pipe` | 函式式 + `pipe` | 鏈式 OO | 裝飾器 class |
66
66
  | 查詢 TS 推導 | 欄位元組支援 | 手動 | 述詞推導 | class-based |
67
- | 多執行緒 | SAB 快照傳輸(0.1);真共享欄位預計 0.2 | SAB-ready,排程自理 | 單執行緒 | Roadmap(未實作) |
67
+ | 多執行緒 | SAB 快照傳輸(0.x);真共享欄位預計 0.3+ | SAB-ready,排程自理 | 單執行緒 | Roadmap(未實作) |
68
68
  | AI 文件 | `llms.txt` + `llms-full.txt` + `api.json` | 無 | 無 | 無 |
69
69
  | 維護狀態 | 活躍(新) | 活躍 | 趨緩(npm 已 ~3 年) | 活躍 |
70
70
 
@@ -293,7 +293,12 @@ const stopAdd = onAdd(world, Position, (e) => console.log('positioned', e))
293
293
  const stopRemove = onRemove(world, Player, (e) => console.log('un-playered', e))
294
294
  const stopSet = onSet(world, Health, (e, val) => console.log('health set', e, val))
295
295
 
296
- // 之後解除
296
+ // AbortSignal 自動解除(0.2.0 起支援):
297
+ const ac = new AbortController()
298
+ onAdd(world, Position, (e) => trackEntity(e), { signal: ac.signal })
299
+ ac.abort() // 一次解除所有掛在此 signal 上的 observer
300
+
301
+ // 也可使用回傳的 unsubscribe,兩種方式皆冪等可混用:
297
302
  stopAdd()
298
303
  stopRemove()
299
304
  stopSet()
@@ -301,6 +306,8 @@ stopSet()
301
306
 
302
307
  Observer 會在變動呼叫內同步觸發。用於需要在變動發生瞬間執行的副作用(除錯、複製)。對於要批次的 UI 更新,請改用反應式查詢。
303
308
 
309
+ **`onSet` 是 low-level mutation hook**,不是反應式 value-predicate query。僅在 `setComponent(world, eid, comp, value)` 且該 entity 已持有該 component 時觸發 ─ `addComponent` 不會觸發 `onSet`(請用 `onAdd`;`addComponent` 後再 `setComponent` 則依序觸發兩者)。`enterQuery` / `exitQuery` 只對 component 集合的結構變化反應;若需要「value 越過閾值」的反應式視圖,請在 app 層基於 `onSet` 自行組裝。
310
+
304
311
  ### Command buffer:何時與為何
305
312
 
306
313
  黃金法則:**不要在正在迭代的實體上新增或移除元件。** 這樣做可能讓某些實體被跳過或被處理兩次,因為原型歸屬中途改變了。請用 command buffer 延後執行:
@@ -343,7 +350,7 @@ addRelation(world, alice, ChildOf, parent)
343
350
  const parentOfAlice = getRelationTargets(world, alice, ChildOf)
344
351
  ```
345
352
 
346
- 0.2 版會加入獨佔關係(只允許一個目標)、wildcard 查詢、以及關係圖的序列化。
353
+ Relations 穩定化 ─ 獨佔關係(只允許一個目標)、wildcard 查詢、以及關係圖的序列化 ─ 預計 **0.3+** 落地;0.2.0 保留 relations subpath 現有 experimental 形狀(無破壞變更)。
347
354
 
348
355
  ## API 參考
349
356
 
@@ -354,7 +361,8 @@ const parentOfAlice = getRelationTargets(world, alice, ChildOf)
354
361
  | 函式 | Signature | 穩定度 |
355
362
  |---|---|---|
356
363
  | `createWorld` | `(options?: WorldOptions) => World` | stable |
357
- | `destroyWorld` | `(world: World) => void` | stable |
364
+ | `disposeWorld` | `(world: World) => void` | stable(since 0.2.0) |
365
+ | `destroyWorld` | `(world: World) => void` | **deprecated** since 0.2.0 ─ `disposeWorld` 的別名;1.0 移除 |
358
366
  | `resetWorld` | `(world: World) => void` | stable |
359
367
  | `getWorldSize` | `(world: World) => number`(存活實體數) | stable |
360
368
  | `getWorldCapacity` | `(world: World) => number` | stable |
@@ -437,10 +445,10 @@ const Types = { i8, u8, i16, u16, i32, u32, f32, f64, eid, bool } as const
437
445
 
438
446
  | 函式 | Signature | 穩定度 |
439
447
  |---|---|---|
440
- | `observe` | `(world, q, event, handler) => () => void` | stable |
441
- | `onAdd` | `(world, comp, handler) => () => void` | stable |
442
- | `onRemove` | `(world, comp, handler) => () => void` | stable |
443
- | `onSet` | `(world, comp, handler) => () => void` | stable |
448
+ | `observe` | `(world, q, event, handler, opts?: { signal? }) => () => void` | stable |
449
+ | `onAdd` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable |
450
+ | `onRemove` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable |
451
+ | `onSet` | `(world, comp, handler, opts?: { signal? }) => () => void` | stable;low-level mutation hook,非反應式 |
444
452
 
445
453
  ### 序列化 — `aiecsjs/serialize`
446
454
 
package/STABILITY.md CHANGED
@@ -25,7 +25,8 @@ The **root** entry (`aiecsjs`) is the stable core: world, entity, component, que
25
25
  | Export | Stability | Since | Notes |
26
26
  |---|---|---|---|
27
27
  | `createWorld` | stable | 0.1.0 | |
28
- | `destroyWorld` | stable | 0.1.0 | |
28
+ | `disposeWorld` | stable | 0.2.0 | Alias for `destroyWorld`; aligns with the ai*js ecosystem `dispose()` convention. Prefer this name in new code. |
29
+ | `destroyWorld` | **deprecated** | 0.1.0 | Use `disposeWorld` instead. Scheduled for removal in 1.0. |
29
30
  | `resetWorld` | stable | 0.1.0 | |
30
31
  | `getWorldSize` | stable | 0.1.0 | |
31
32
  | `getWorldCapacity` | stable | 0.1.0 | |
@@ -33,8 +34,8 @@ The **root** entry (`aiecsjs`) is the stable core: world, entity, component, que
33
34
  | `destroyEntity` | stable | 0.1.0 | |
34
35
  | `entityExists` | stable | 0.1.0 | |
35
36
  | `getEntityIndex` | stable | 0.1.0 | |
36
- | `getEntityGeneration` | stable | 0.1.0 | |
37
- | `packEntity` | stable | 0.1.0 | |
37
+ | `getEntityGeneration` | **experimental** | 0.1.0 | Returns 0 in 0.x (generation tracked internally but not encoded in EntityId). Real values arrive with ABA-safe `EntityRef` in **0.3+**. |
38
+ | `packEntity` | **experimental** | 0.1.0 | Identity helper in 0.x. Returns the index unchanged. Real packing arrives with `EntityRef` in **0.3+**. |
38
39
  | `defineComponent` | stable | 0.1.0 | |
39
40
  | `defineTag` | stable | 0.1.0 | |
40
41
  | `defineObjectComponent` | stable | 0.1.0 | AoS components are main-thread only; not SAB-shareable. |
@@ -81,10 +82,10 @@ Component lifecycle hooks. The core does not require observers; install this sub
81
82
 
82
83
  | Export | Stability | Since | Notes |
83
84
  |---|---|---|---|
84
- | `observe` | stable | 0.1.0 | |
85
- | `onAdd` | stable | 0.1.0 | |
86
- | `onRemove` | stable | 0.1.0 | |
87
- | `onSet` | stable | 0.1.0 | |
85
+ | `observe` | stable | 0.1.0 | Accepts `{ signal?: AbortSignal }` since 0.2.0. |
86
+ | `onAdd` | stable | 0.1.0 | Accepts `{ signal?: AbortSignal }` since 0.2.0. |
87
+ | `onRemove` | stable | 0.1.0 | Accepts `{ signal?: AbortSignal }` since 0.2.0. |
88
+ | `onSet` | stable | 0.1.0 | Low-level mutation hook; NOT a reactive value-predicate query. Accepts `{ signal?: AbortSignal }` since 0.2.0. |
88
89
 
89
90
  ### `aiecsjs/serialize` (utility sub-path)
90
91
 
@@ -98,7 +99,7 @@ Component lifecycle hooks. The core does not require observers; install this sub
98
99
 
99
100
  ### `aiecsjs/worker` (experimental adapter sub-path)
100
101
 
101
- The entire subpath is **experimental** in 0.1. **In 0.1 the implementation is a snapshot-copy transport** — serialize the world into the SAB on send, deserialize into a fresh world on adopt. It is not true shared-memory column aliasing. The API surface matches the documented contract; true shared columns ship in 0.2. Snapshot layout and capability flags may change.
102
+ The entire subpath is **experimental** in 0.x. **In 0.x the implementation is a snapshot-copy transport** — serialize the world into the SAB on send, deserialize into a fresh world on adopt. It is not true shared-memory column aliasing. The API surface matches the documented contract; true shared columns are targeted for **0.3+**. Snapshot layout and capability flags may change.
102
103
 
103
104
  | Export | Stability | Since | Notes |
104
105
  |---|---|---|---|
@@ -128,7 +129,8 @@ Everything under this prefix is **internal**. It exists for the implementation's
128
129
  | Version | Focus | Stability shift |
129
130
  |---|---|---|
130
131
  | 0.1.x | Core surface (world, entity, component, query, system, loop, commands, observers, serialize) | Initial publish; all marked experimental at the package level but per-export stable where listed. |
131
- | 0.2.x | Relations & hierarchies | `aiecsjs/relations` becomes implemented; still experimental. |
132
+ | 0.2.0 | Safety + alignment | Prototype-pollution hardening, observer `{ signal? }`, `disposeWorld` alias, `getEntityGeneration` / `packEntity` re-labelled experimental, `verify:llms` gate. See [CHANGELOG.md](./CHANGELOG.md#020---2026-05-28). |
133
+ | 0.3+ | Relations stabilisation + EntityRef + SAB | `aiecsjs/relations` graduates to stable; ABA-safe `EntityRef` lands and `getEntityGeneration` / `packEntity` start returning real values; `aiecsjs/worker` adopts true shared-memory column aliasing. |
132
134
  | 0.3.x | Hardening, relations stabilization, multi-threading polish | `aiecsjs/relations` and `aiecsjs/worker` → stable. |
133
135
  | 1.0.0 | API freeze | All `stable` exports frozen for 1.x. |
134
136
 
package/STABILITY_ZHTW.md CHANGED
@@ -98,7 +98,7 @@ aiecsjs 遵循 [semver](https://semver.org/)。在 **0.x** 系列內:
98
98
 
99
99
  ### `aiecsjs/worker`(experimental adapter sub-path)
100
100
 
101
- 整個 subpath 在 0.1 為 **experimental**。**0.1 的實作為 snapshot-copy 傳輸**——傳送時將 world 序列化進 SAB,接收端反序列化成全新 world,並非真正的共享記憶體欄位 aliasing。API 表面符合文件契約;真正的共享欄位將於 0.2 推出。Snapshot 佈局與能力旗標可能改變。
101
+ 整個 subpath 在 0.x 為 **experimental**。**0.x 的實作為 snapshot-copy 傳輸**——傳送時將 world 序列化進 SAB,接收端反序列化成全新 world,並非真正的共享記憶體欄位 aliasing。API 表面符合文件契約;真正的共享欄位預計 **0.3+** 推出。Snapshot 佈局與能力旗標可能改變。
102
102
 
103
103
  | 匯出 | 穩定度 | 起始版本 | 備註 |
104
104
  |---|---|---|---|
@@ -128,8 +128,8 @@ aiecsjs 遵循 [semver](https://semver.org/)。在 **0.x** 系列內:
128
128
  | 版本 | 焦點 | 穩定度變動 |
129
129
  |---|---|---|
130
130
  | 0.1.x | 核心表面(world、entity、component、query、system、loop、commands、observers、serialize) | 初次發佈;package 整體標 experimental,但各 export 表中列為 stable 者皆穩定。 |
131
- | 0.2.x | Relations 與階層 | `aiecsjs/relations` 落實實作;仍為 experimental。 |
132
- | 0.3.x | 硬化、Relations 穩定、多執行緒打磨 | `aiecsjs/relations` 與 `aiecsjs/worker` → stable。 |
131
+ | 0.2.0 | 安全與生態對齊 | 原型污染強化、observer `{ signal? }`、`disposeWorld` 別名、`getEntityGeneration` / `packEntity` 改 experimental、`verify:llms` gate。詳見 [CHANGELOG.md](./CHANGELOG.md#020---2026-05-28)。 |
132
+ | 0.3+ | Relations 穩定化 + EntityRef + SAB | `aiecsjs/relations` 升為 stable;ABA-safe `EntityRef` 上線,`getEntityGeneration` / `packEntity` 開始回傳真值;`aiecsjs/worker` 採用真正 shared-memory column aliasing。 |
133
133
  | 1.0.0 | API 凍結 | 所有 `stable` 匯出於 1.x 系列凍結。 |
134
134
 
135
135
  ## 在執行時檢查穩定度
package/api.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "name": "aiecsjs",
13
13
  "description": "Core entry point — world, entity, component, query, system, utilities.",
14
14
  "exports": [
15
- "createWorld", "destroyWorld", "resetWorld", "getWorldSize", "getWorldCapacity",
15
+ "createWorld", "disposeWorld", "destroyWorld", "resetWorld", "getWorldSize", "getWorldCapacity",
16
16
  "createEntity", "destroyEntity", "entityExists", "getEntityIndex", "getEntityGeneration", "packEntity",
17
17
  "defineComponent", "defineTag", "defineObjectComponent",
18
18
  "addComponent", "removeComponent", "hasComponent", "getComponent", "setComponent",
@@ -84,13 +84,29 @@
84
84
  "kind": "function",
85
85
  "module": "aiecsjs",
86
86
  "since": "0.1.0",
87
- "stability": "stable",
88
- "deprecated": false,
87
+ "stability": "deprecated",
88
+ "deprecated": true,
89
+ "deprecatedSince": "0.2.0",
90
+ "replacedBy": "disposeWorld",
89
91
  "signature": "function destroyWorld(world: World): void",
90
- "summary": "Release all internal buffers; the world reference becomes invalid.",
92
+ "summary": "Release all internal buffers; the world reference becomes invalid. Use `disposeWorld` in new code; this alias is retained for backwards compatibility and is scheduled for removal in 1.0.",
91
93
  "params": [{ "name": "world", "type": "World", "optional": false, "description": "World to destroy." }],
92
94
  "returns": { "type": "void", "description": "" },
93
- "examples": [{ "title": "Cleanup", "code": "destroyWorld(world)" }],
95
+ "examples": [{ "title": "Cleanup (deprecated form)", "code": "destroyWorld(world) // prefer disposeWorld(world)" }],
96
+ "seeAlso": ["disposeWorld", "createWorld", "resetWorld"]
97
+ },
98
+ {
99
+ "name": "disposeWorld",
100
+ "kind": "function",
101
+ "module": "aiecsjs",
102
+ "since": "0.2.0",
103
+ "stability": "stable",
104
+ "deprecated": false,
105
+ "signature": "function disposeWorld(world: World): void",
106
+ "summary": "Release all internal buffers; the world reference becomes invalid. Aligns with the ai*js ecosystem `dispose()` convention (aifsmjs Runtime, aibridgejs Bridge).",
107
+ "params": [{ "name": "world", "type": "World", "optional": false, "description": "World to dispose." }],
108
+ "returns": { "type": "void", "description": "" },
109
+ "examples": [{ "title": "Cleanup", "code": "disposeWorld(world)" }],
94
110
  "seeAlso": ["createWorld", "resetWorld"]
95
111
  },
96
112
  {
@@ -203,12 +219,12 @@
203
219
  "kind": "function",
204
220
  "module": "aiecsjs",
205
221
  "since": "0.1.0",
206
- "stability": "stable",
222
+ "stability": "experimental",
207
223
  "deprecated": false,
208
224
  "signature": "function getEntityGeneration(eid: EntityId): number",
209
- "summary": "Extract the generation portion of a packed entity ID.",
225
+ "summary": "Returns 0 in 0.x — generation is tracked internally for slot reuse but not encoded in the EntityId. Real generation reads land with ABA-safe `EntityRef` in 0.3+.",
210
226
  "params": [{ "name": "eid", "type": "EntityId", "optional": false, "description": "Packed entity ID." }],
211
- "returns": { "type": "number", "description": "Generation in [0, 2^generationBits)." },
227
+ "returns": { "type": "number", "description": "Generation; always 0 in 0.1." },
212
228
  "examples": [],
213
229
  "seeAlso": ["getEntityIndex", "packEntity"]
214
230
  },
@@ -217,15 +233,15 @@
217
233
  "kind": "function",
218
234
  "module": "aiecsjs",
219
235
  "since": "0.1.0",
220
- "stability": "stable",
236
+ "stability": "experimental",
221
237
  "deprecated": false,
222
238
  "signature": "function packEntity(index: number, generation: number): EntityId",
223
- "summary": "Compose an entity ID from its parts. Rarely needed.",
239
+ "summary": "Identity helper in 0.x — returns `index` unchanged because EntityId is a bare slot index. Real packing lands with ABA-safe `EntityRef` in 0.3+.",
224
240
  "params": [
225
241
  { "name": "index", "type": "number", "optional": false, "description": "Entity index." },
226
- { "name": "generation", "type": "number", "optional": false, "description": "Entity generation." }
242
+ { "name": "generation", "type": "number", "optional": false, "description": "Entity generation (ignored in 0.1)." }
227
243
  ],
228
- "returns": { "type": "EntityId", "description": "Branded composed ID." },
244
+ "returns": { "type": "EntityId", "description": "In 0.1 this equals `index`." },
229
245
  "examples": [],
230
246
  "seeAlso": ["getEntityIndex", "getEntityGeneration"]
231
247
  },
@@ -606,13 +622,14 @@
606
622
  "since": "0.1.0",
607
623
  "stability": "stable",
608
624
  "deprecated": false,
609
- "signature": "function observe(world: World, query: Query, event: 'add' | 'remove' | 'set', handler: (eid: EntityId) => void): () => void",
610
- "summary": "General-purpose query observer. Returns a disposer.",
625
+ "signature": "function observe(world: World, query: Query, event: 'add' | 'remove' | 'set', handler: (eid: EntityId) => void, opts?: ObserverOptions): () => void",
626
+ "summary": "General-purpose query observer. Returns a disposer. `opts.signal` (since 0.2.0) auto-unsubscribes when aborted.",
611
627
  "params": [
612
628
  { "name": "world", "type": "World", "optional": false, "description": "Target world." },
613
629
  { "name": "query", "type": "Query", "optional": false, "description": "Query handle." },
614
630
  { "name": "event", "type": "'add'|'remove'|'set'", "optional": false, "description": "Event kind." },
615
- { "name": "handler", "type": "(eid) => void", "optional": false, "description": "Callback." }
631
+ { "name": "handler", "type": "(eid) => void", "optional": false, "description": "Callback." },
632
+ { "name": "opts", "type": "ObserverOptions", "optional": true, "description": "Optional `{ signal?: AbortSignal }`. Added in 0.2.0." }
616
633
  ],
617
634
  "returns": { "type": "() => void", "description": "Disposer." },
618
635
  "examples": [],
@@ -625,12 +642,13 @@
625
642
  "since": "0.1.0",
626
643
  "stability": "stable",
627
644
  "deprecated": false,
628
- "signature": "function onAdd(world: World, component: ComponentLike, handler: (eid: EntityId) => void): () => void",
629
- "summary": "Listen for component-add events on a specific component.",
645
+ "signature": "function onAdd(world: World, component: ComponentLike, handler: (eid: EntityId) => void, opts?: ObserverOptions): () => void",
646
+ "summary": "Listen for component-add events on a specific component. `opts.signal` (since 0.2.0) auto-unsubscribes when aborted.",
630
647
  "params": [
631
648
  { "name": "world", "type": "World", "optional": false, "description": "Target world." },
632
649
  { "name": "component", "type": "ComponentLike", "optional": false, "description": "Component handle." },
633
- { "name": "handler", "type": "(eid) => void", "optional": false, "description": "Callback." }
650
+ { "name": "handler", "type": "(eid) => void", "optional": false, "description": "Callback." },
651
+ { "name": "opts", "type": "ObserverOptions", "optional": true, "description": "Optional `{ signal?: AbortSignal }`. Added in 0.2.0." }
634
652
  ],
635
653
  "returns": { "type": "() => void", "description": "Disposer." },
636
654
  "examples": [],
@@ -643,9 +661,14 @@
643
661
  "since": "0.1.0",
644
662
  "stability": "stable",
645
663
  "deprecated": false,
646
- "signature": "function onRemove(world: World, component: ComponentLike, handler: (eid: EntityId) => void): () => void",
647
- "summary": "Listen for component-remove events.",
648
- "params": [],
664
+ "signature": "function onRemove(world: World, component: ComponentLike, handler: (eid: EntityId) => void, opts?: ObserverOptions): () => void",
665
+ "summary": "Listen for component-remove events. `opts.signal` (since 0.2.0) auto-unsubscribes when aborted.",
666
+ "params": [
667
+ { "name": "world", "type": "World", "optional": false, "description": "Target world." },
668
+ { "name": "component", "type": "ComponentLike", "optional": false, "description": "Component handle." },
669
+ { "name": "handler", "type": "(eid) => void", "optional": false, "description": "Callback." },
670
+ { "name": "opts", "type": "ObserverOptions", "optional": true, "description": "Optional `{ signal?: AbortSignal }`. Added in 0.2.0." }
671
+ ],
649
672
  "returns": { "type": "() => void", "description": "Disposer." },
650
673
  "examples": [],
651
674
  "seeAlso": ["onAdd", "onSet"]
@@ -657,9 +680,14 @@
657
680
  "since": "0.1.0",
658
681
  "stability": "stable",
659
682
  "deprecated": false,
660
- "signature": "function onSet<C extends ComponentLike>(world: World, component: C, handler: (eid: EntityId, value: unknown) => void): () => void",
661
- "summary": "Listen for setComponent calls.",
662
- "params": [],
683
+ "signature": "function onSet<C extends ComponentLike>(world: World, component: C, handler: (eid: EntityId, value: unknown) => void, opts?: ObserverOptions): () => void",
684
+ "summary": "Low-level mutation hook — fires after `setComponent` when the component is already present on the entity (an `addComponent` followed by `setComponent` fires onAdd then onSet). NOT a reactive value-predicate query. `opts.signal` (since 0.2.0) auto-unsubscribes when aborted.",
685
+ "params": [
686
+ { "name": "world", "type": "World", "optional": false, "description": "Target world." },
687
+ { "name": "component", "type": "C", "optional": false, "description": "Component handle." },
688
+ { "name": "handler", "type": "(eid, value) => void", "optional": false, "description": "Callback receiving the new value." },
689
+ { "name": "opts", "type": "ObserverOptions", "optional": true, "description": "Optional `{ signal?: AbortSignal }`. Added in 0.2.0." }
690
+ ],
663
691
  "returns": { "type": "() => void", "description": "Disposer." },
664
692
  "examples": [],
665
693
  "seeAlso": ["onAdd", "onRemove"]
package/dist/commands.cjs CHANGED
@@ -1,2 +1,2 @@
1
- 'use strict';function C(e,t){let n=t>>>5;e[n]=(e[n]??0)|1<<(t&31);}function g(e,t){let n=t>>>5;e[n]=(e[n]??0)&~(1<<(t&31));}function b(e,t){let n=t>>>5;return ((e[n]??0)&1<<(t&31))!==0}function y(e){let t=new Uint32Array(e.length);for(let n=0;n<e.length;n++)t[n]=e[n]??0;return t}function x(e){let t=[];for(let n=0;n<e.length;n++)t.push((e[n]??0).toString(16));return t.join(",")}function k(e,t,n,o){for(let r=0;r<n;r++){let i=e[t+r]??0;for(;i!==0;){let s=(r<<5)+$(i);o(s),i&=i-1;}}}function $(e){return 31-Math.clz32(e&-e)}var H=new Map;function u(e){let t=H.get(e.id);if(!t)throw new Error(`aiecsjs: world ${e.id} is destroyed or unknown`);if(t.destroyed)throw new Error(`aiecsjs: world ${e.id} is destroyed`);return t}function I(e,t){if(t<=e.capacity)return;if(t>e.options.maxEntities)throw new Error(`aiecsjs: requested capacity ${t} exceeds maxEntities ${e.options.maxEntities}`);let n=e.capacity;for(;n<t;)n=Math.min(n*2,e.options.maxEntities);N(e,n);}function N(e,t){let n=e.generations.constructor,o=new n(t);o.set(e.generations),e.generations=o;let r=new Uint32Array(t);r.set(e.entityArchetype),e.entityArchetype=r;let i=e.options.maskWordCount,s=new Uint32Array(t*i);s.set(e.entityMask),e.entityMask=s;for(let a of e.componentStorageByBit)if(a)if(a.kind==="soa"&&a.soa){let c=e.componentInfoByBit[a.bit];c&&V(a.soa,c.fields,t);}else a.kind==="aos"&&a.aos&&(a.aos.length=t);e.capacity=t;}function V(e,t,n){for(let o of t){let r=e[o.name];if(!r)continue;let i=n*o.vectorLen;if(r.length>=i)continue;let s=new o.ctor(i);s.set(r),e[o.name]=s;}}function v(e,t){let n=x(t),o=e.archetypeByMaskHash.get(n);if(o!==void 0)return {archId:o,created:false};let r=e.archetypes.length,i={id:r,mask:y(t),size:0,capacity:16,entities:new Uint32Array(16),entityRow:new Map,componentBits:G(t),edgeAdd:new Int32Array(e.options.maxComponents).fill(-1),edgeRemove:new Int32Array(e.options.maxComponents).fill(-1)};return e.archetypes.push(i),e.archetypeByMaskHash.set(n,r),e.queryVersion++,{archId:r,created:true}}function m(e,t){if(t<=e.capacity)return;let n=e.capacity;for(;n<t;)n*=2;let o=new Uint32Array(n);o.set(e.entities),e.entities=o,e.capacity=n;}function G(e){let t=[];for(let n=0;n<e.length;n++){let o=e[n]??0;for(;o!==0;){let r=o&-o,i=(n<<5)+(31-Math.clz32(r));t.push(i),o&=o-1;}}return t}function E(e,t){let n=e.componentBitFor.get(t.id);if(n!==void 0)return n;if(e.nextComponentBit>=e.options.maxComponents)throw new Error(`aiecsjs: world reached maxComponents=${e.options.maxComponents}`);let o=e.nextComponentBit++;e.componentBitFor.set(t.id,o),e.componentInfoByBit[o]=t;let r;if(t.kind==="soa"){let i={};for(let s of t.fields)i[s.name]=new s.ctor(e.capacity*s.vectorLen);r={kind:"soa",componentId:t.id,bit:o,soa:i};}else t.kind==="aos"?r={kind:"aos",componentId:t.id,bit:o,aos:new Array(e.capacity)}:r={kind:"tag",componentId:t.id,bit:o};return e.componentStorageByBit[o]=r,o}function w(e,t){let n=e.options.maskWordCount,o=new Uint32Array(n),r=t*n;for(let i=0;i<n;i++)o[i]=e.entityMask[r+i]??0;return o}function S(e,t,n){let o=e.options.maskWordCount,r=t*o;for(let i=0;i<o;i++)e.entityMask[r+i]=n[i]??0;}function B(e,t){return e.componentBitFor.get(t.id)}function U(e){let t=u(e);if(t.readOnly)throw new Error("aiecsjs: cannot createEntity on a read-only world (worker-attached)");let n;if(t.freeList.length>0)n=t.freeList.pop();else {if(t.nextFreshIndex>=t.options.maxEntities)throw new Error(`aiecsjs: reached maxEntities ${t.options.maxEntities}`);t.nextFreshIndex>=t.capacity&&I(t,t.nextFreshIndex+1),n=t.nextFreshIndex++;}let o=t.archetypes[0];if(!o)throw new Error("aiecsjs: missing empty archetype");m(o,o.size+1);let r=o.size;o.entities[r]=n,o.entityRow.set(n,r),o.size++,t.entityArchetype[n]=0;let i=t.options.maskWordCount,s=n*i;for(let a=0;a<i;a++)t.entityMask[s+a]=0;return t.size++,n}function O(e,t){let n=u(e);if(n.readOnly)throw new Error("aiecsjs: cannot destroyEntity on a read-only world");if(!h(n,t))return;R(n,t);let r=n.entityArchetype[t]??0,i=n.archetypes[r];if(i){let f=i.entityRow.get(t);if(f!==void 0){let d=i.size-1;if(f!==d){let l=i.entities[d]??0;i.entities[f]=l,i.entityRow.set(l,f);}i.entities[d]=0,i.entityRow.delete(t),i.size--;}}n.entityArchetype[t]=0;let s=n.options.maskWordCount,a=t*s;for(let f=0;f<s;f++)n.entityMask[a+f]=0;let c=t;n.generations[c]=(n.generations[c]??0)+1&65535,n.freeList.push(t),n.size--;}function h(e,t){if(t<=0||t>=e.capacity)return false;let n=e.entityArchetype[t]??0,o=e.archetypes[n];return o?o.entityRow.has(t):false}var X=new Map;function A(e){let t=X.get(e.__id);if(!t)throw new Error("aiecsjs: component is not registered (call defineComponent/defineTag/defineObjectComponent)");return t}function _(e,t,n,o){let r=u(e);if(r.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");if(!h(r,t))throw new Error(`aiecsjs: addComponent on dead entity ${t}`);let i=A(n),s=E(r,i),a=w(r,t);if(b(a,s)){o!==void 0&&j(r,t,n,o);return}let c=y(a);C(c,s),T(r,t,c),j(r,t,n,o);}function L(e,t,n){let o=u(e);if(o.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");if(!h(o,t))return;let r=A(n),i=B(o,r);if(i===void 0)return;let s=w(o,t);if(!b(s,i))return;let a=y(s);g(a,i),T(o,t,a);let c=o.componentStorageByBit[i];c?.kind==="soa"&&c.soa?F(c.soa,r.fields,t):c?.kind==="aos"&&c.aos&&(c.aos[t]=void 0);}function j(e,t,n,o){let r=A(n),i=e.componentBitFor.get(r.id);if(i===void 0)return;let s=e.componentStorageByBit[i];if(s){if(r.kind==="soa"&&s.soa){if(o==null)return;let a=o;for(let c of r.fields){if(!(c.name in a))continue;let f=s.soa[c.name];if(!f)continue;let d=a[c.name];if(c.vectorLen===1)f[t]=typeof d=="boolean"?d?1:0:Number(d);else if(Array.isArray(d)||ArrayBuffer.isView(d)){let l=t*c.vectorLen,D=d;for(let p=0;p<c.vectorLen;p++)f[l+p]=Number(D[p]??0);}}}else if(r.kind==="aos"&&s.aos){let a=r.factory??(()=>({})),c=s.aos[t];c===void 0&&(c=a(),s.aos[t]=c),o&&typeof o=="object"&&Object.assign(c,o);}}}function F(e,t,n){for(let o of t){let r=e[o.name];if(r)if(o.vectorLen===1)r[n]=0;else {let i=n*o.vectorLen;for(let s=0;s<o.vectorLen;s++)r[i+s]=0;}}}function R(e,t){let n=e.options.maskWordCount,o=t,r=o*n;k(e.entityMask,r,n,i=>{let s=e.componentStorageByBit[i],a=e.componentInfoByBit[i];s?.kind==="soa"&&s.soa&&a?F(s.soa,a.fields,o):s?.kind==="aos"&&s.aos&&(s.aos[o]=void 0);});}function T(e,t,n){let o=e.entityArchetype[t]??0,r=e.archetypes[o];if(!r)return;let s=v(e,n).archId,a=e.archetypes[s];if(a){if(s!==o){let c=r.entityRow.get(t);if(c!==void 0){let d=r.size-1;if(c!==d){let l=r.entities[d]??0;r.entities[c]=l,r.entityRow.set(l,c);}r.entities[d]=0,r.entityRow.delete(t),r.size--;}m(a,a.size+1);let f=a.size;a.entities[f]=t,a.entityRow.set(t,f),a.size++,e.entityArchetype[t]=s;}S(e,t,n);}}function tt(e){let n={worldId:u(e).id,ops:[],nextPlaceholder:-1,flushing:false};return nt(n)}function et(e){let t=ot(e);if(!t.flushing){t.flushing=true;try{let n=rt(t.worldId),o=new Map;for(let i of t.ops)i.kind==="create"&&o.set(i.placeholder,U(n));let r=i=>{let s=i;if(s<0){let a=o.get(s);if(a===void 0)throw new Error(`aiecsjs: unresolved placeholder ${s}`);return a}return i};for(let i of t.ops)i.kind==="add"?_(n,r(i.eid),i.component,i.initial):i.kind==="remove"&&L(n,r(i.eid),i.component);for(let i of t.ops)i.kind==="destroy"&&O(n,r(i.eid));t.ops.length=0,t.nextPlaceholder=-1;}finally{t.flushing=false;}}}function gt(e,t){let n=tt(e),o=t(n);return et(n),o}var q=new WeakMap;function nt(e){let t={add(n,o,r){e.ops.push({kind:"add",eid:n,component:o,initial:r});},remove(n,o){e.ops.push({kind:"remove",eid:n,component:o});},destroy(n){e.ops.push({kind:"destroy",eid:n});},create(){let n=e.nextPlaceholder--;return e.ops.push({kind:"create",placeholder:n}),n}};return q.set(t,e),t}function ot(e){let t=q.get(e);if(!t)throw new Error("aiecsjs: unknown CommandBuffer");return t}function rt(e){let t=it(e);return {id:t.id,capacity:t.capacity,version:t.version}}function it(e){return u({id:e})}exports.createCommandBuffer=tt;exports.flush=et;exports.withCommandBuffer=gt;//# sourceMappingURL=commands.cjs.map
1
+ 'use strict';function C(n,t){let e=t>>>5;n[e]=(n[e]??0)|1<<(t&31);}function g(n,t){let e=t>>>5;n[e]=(n[e]??0)&~(1<<(t&31));}function b(n,t){let e=t>>>5;return ((n[e]??0)&1<<(t&31))!==0}function y(n){let t=new Uint32Array(n.length);for(let e=0;e<n.length;e++)t[e]=n[e]??0;return t}function x(n){let t=[];for(let e=0;e<n.length;e++)t.push((n[e]??0).toString(16));return t.join(",")}function k(n,t,e,o){for(let r=0;r<e;r++){let i=n[t+r]??0;for(;i!==0;){let s=(r<<5)+$(i);o(s),i&=i-1;}}}function $(n){return 31-Math.clz32(n&-n)}var H=new Map;function l(n){let t=H.get(n.id);if(!t)throw new Error(`aiecsjs: world ${n.id} is destroyed or unknown`);if(t.destroyed)throw new Error(`aiecsjs: world ${n.id} is destroyed`);return t}function I(n,t){if(t<=n.capacity)return;if(t>n.options.maxEntities)throw new Error(`aiecsjs: requested capacity ${t} exceeds maxEntities ${n.options.maxEntities}`);let e=n.capacity;for(;e<t;)e=Math.min(e*2,n.options.maxEntities);N(n,e);}function N(n,t){let e=n.generations.constructor,o=new e(t);o.set(n.generations),n.generations=o;let r=new Uint32Array(t);r.set(n.entityArchetype),n.entityArchetype=r;let i=n.options.maskWordCount,s=new Uint32Array(t*i);s.set(n.entityMask),n.entityMask=s;for(let a of n.componentStorageByBit)if(a)if(a.kind==="soa"&&a.soa){let c=n.componentInfoByBit[a.bit];c&&V(a.soa,c.fields,t);}else a.kind==="aos"&&a.aos&&(a.aos.length=t);n.capacity=t;}function V(n,t,e){for(let o of t){let r=n[o.name];if(!r)continue;let i=e*o.vectorLen;if(r.length>=i)continue;let s=new o.ctor(i);s.set(r),n[o.name]=s;}}function v(n,t){let e=x(t),o=n.archetypeByMaskHash.get(e);if(o!==void 0)return {archId:o,created:false};let r=n.archetypes.length,i={id:r,mask:y(t),size:0,capacity:16,entities:new Uint32Array(16),entityRow:new Map,componentBits:G(t),edgeAdd:new Int32Array(n.options.maxComponents).fill(-1),edgeRemove:new Int32Array(n.options.maxComponents).fill(-1)};return n.archetypes.push(i),n.archetypeByMaskHash.set(e,r),n.queryVersion++,{archId:r,created:true}}function m(n,t){if(t<=n.capacity)return;let e=n.capacity;for(;e<t;)e*=2;let o=new Uint32Array(e);o.set(n.entities),n.entities=o,n.capacity=e;}function G(n){let t=[];for(let e=0;e<n.length;e++){let o=n[e]??0;for(;o!==0;){let r=o&-o,i=(e<<5)+(31-Math.clz32(r));t.push(i),o&=o-1;}}return t}function E(n,t){let e=n.componentBitFor.get(t.id);if(e!==void 0)return e;if(n.nextComponentBit>=n.options.maxComponents)throw new Error(`aiecsjs: world reached maxComponents=${n.options.maxComponents}`);let o=n.nextComponentBit++;n.componentBitFor.set(t.id,o),n.componentInfoByBit[o]=t;let r;if(t.kind==="soa"){let i={};for(let s of t.fields)i[s.name]=new s.ctor(n.capacity*s.vectorLen);r={kind:"soa",componentId:t.id,bit:o,soa:i};}else t.kind==="aos"?r={kind:"aos",componentId:t.id,bit:o,aos:new Array(n.capacity)}:r={kind:"tag",componentId:t.id,bit:o};return n.componentStorageByBit[o]=r,o}function w(n,t){let e=n.options.maskWordCount,o=new Uint32Array(e),r=t*e;for(let i=0;i<e;i++)o[i]=n.entityMask[r+i]??0;return o}function S(n,t,e){let o=n.options.maskWordCount,r=t*o;for(let i=0;i<o;i++)n.entityMask[r+i]=e[i]??0;}function B(n,t){return n.componentBitFor.get(t.id)}function U(n){let t=l(n);if(t.readOnly)throw new Error("aiecsjs: cannot createEntity on a read-only world (worker-attached)");let e;if(t.freeList.length>0)e=t.freeList.pop();else {if(t.nextFreshIndex>=t.options.maxEntities)throw new Error(`aiecsjs: reached maxEntities ${t.options.maxEntities}`);t.nextFreshIndex>=t.capacity&&I(t,t.nextFreshIndex+1),e=t.nextFreshIndex++;}let o=t.archetypes[0];if(!o)throw new Error("aiecsjs: missing empty archetype");m(o,o.size+1);let r=o.size;o.entities[r]=e,o.entityRow.set(e,r),o.size++,t.entityArchetype[e]=0;let i=t.options.maskWordCount,s=e*i;for(let a=0;a<i;a++)t.entityMask[s+a]=0;return t.size++,e}function O(n,t){let e=l(n);if(e.readOnly)throw new Error("aiecsjs: cannot destroyEntity on a read-only world");if(!h(e,t))return;R(e,t);let r=e.entityArchetype[t]??0,i=e.archetypes[r];if(i){let f=i.entityRow.get(t);if(f!==void 0){let d=i.size-1;if(f!==d){let u=i.entities[d]??0;i.entities[f]=u,i.entityRow.set(u,f);}i.entities[d]=0,i.entityRow.delete(t),i.size--;}}e.entityArchetype[t]=0;let s=e.options.maskWordCount,a=t*s;for(let f=0;f<s;f++)e.entityMask[a+f]=0;let c=t;e.generations[c]=(e.generations[c]??0)+1&65535,e.freeList.push(t),e.size--;}function h(n,t){if(t<=0||t>=n.capacity)return false;let e=n.entityArchetype[t]??0,o=n.archetypes[e];return o?o.entityRow.has(t):false}var X=new Map;function A(n){let t=X.get(n.__id);if(!t)throw new Error("aiecsjs: component is not registered (call defineComponent/defineTag/defineObjectComponent)");return t}function j(n,t,e,o){let r=l(n);if(r.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");if(!h(r,t))throw new Error(`aiecsjs: addComponent on dead entity ${t}`);let i=A(e),s=E(r,i),a=w(r,t);if(b(a,s)){o!==void 0&&_(r,t,e,o);return}let c=y(a);C(c,s),F(r,t,c),_(r,t,e,o);}function L(n,t,e){let o=l(n);if(o.readOnly)throw new Error("aiecsjs: cannot mutate a read-only world");if(!h(o,t))return;let r=A(e),i=B(o,r);if(i===void 0)return;let s=w(o,t);if(!b(s,i))return;let a=y(s);g(a,i),F(o,t,a);let c=o.componentStorageByBit[i];c?.kind==="soa"&&c.soa?T(c.soa,r.fields,t):c?.kind==="aos"&&c.aos&&(c.aos[t]=void 0);}function _(n,t,e,o){let r=A(e),i=n.componentBitFor.get(r.id);if(i===void 0)return;let s=n.componentStorageByBit[i];if(s){if(r.kind==="soa"&&s.soa){if(o==null)return;let a=o;for(let c of r.fields){if(!(c.name in a))continue;let f=s.soa[c.name];if(!f)continue;let d=a[c.name];if(c.vectorLen===1)f[t]=typeof d=="boolean"?d?1:0:Number(d);else if(Array.isArray(d)||ArrayBuffer.isView(d)){let u=t*c.vectorLen,D=d;for(let p=0;p<c.vectorLen;p++)f[u+p]=Number(D[p]??0);}}}else if(r.kind==="aos"&&s.aos){let a=r.factory??(()=>({})),c=s.aos[t];if(c===void 0&&(c=a(),s.aos[t]=c),o&&typeof o=="object"){let f=o,d=c;for(let u of Object.keys(f))u==="__proto__"||u==="constructor"||u==="prototype"||(d[u]=f[u]);}}}}function T(n,t,e){for(let o of t){let r=n[o.name];if(r)if(o.vectorLen===1)r[e]=0;else {let i=e*o.vectorLen;for(let s=0;s<o.vectorLen;s++)r[i+s]=0;}}}function R(n,t){let e=n.options.maskWordCount,o=t,r=o*e;k(n.entityMask,r,e,i=>{let s=n.componentStorageByBit[i],a=n.componentInfoByBit[i];s?.kind==="soa"&&s.soa&&a?T(s.soa,a.fields,o):s?.kind==="aos"&&s.aos&&(s.aos[o]=void 0);});}function F(n,t,e){let o=n.entityArchetype[t]??0,r=n.archetypes[o];if(!r)return;let s=v(n,e).archId,a=n.archetypes[s];if(a){if(s!==o){let c=r.entityRow.get(t);if(c!==void 0){let d=r.size-1;if(c!==d){let u=r.entities[d]??0;r.entities[c]=u,r.entityRow.set(u,c);}r.entities[d]=0,r.entityRow.delete(t),r.size--;}m(a,a.size+1);let f=a.size;a.entities[f]=t,a.entityRow.set(t,f),a.size++,n.entityArchetype[t]=s;}S(n,t,e);}}function tt(n){let e={worldId:l(n).id,ops:[],nextPlaceholder:-1,flushing:false};return et(e)}function nt(n){let t=ot(n);if(!t.flushing){t.flushing=true;try{let e=rt(t.worldId),o=new Map;for(let i of t.ops)i.kind==="create"&&o.set(i.placeholder,U(e));let r=i=>{let s=i;if(s<0){let a=o.get(s);if(a===void 0)throw new Error(`aiecsjs: unresolved placeholder ${s}`);return a}return i};for(let i of t.ops)i.kind==="add"?j(e,r(i.eid),i.component,i.initial):i.kind==="remove"&&L(e,r(i.eid),i.component);for(let i of t.ops)i.kind==="destroy"&&O(e,r(i.eid));t.ops.length=0,t.nextPlaceholder=-1;}finally{t.flushing=false;}}}function gt(n,t){let e=tt(n),o=t(e);return nt(e),o}var q=new WeakMap;function et(n){let t={add(e,o,r){n.ops.push({kind:"add",eid:e,component:o,initial:r});},remove(e,o){n.ops.push({kind:"remove",eid:e,component:o});},destroy(e){n.ops.push({kind:"destroy",eid:e});},create(){let e=n.nextPlaceholder--;return n.ops.push({kind:"create",placeholder:e}),e}};return q.set(t,n),t}function ot(n){let t=q.get(n);if(!t)throw new Error("aiecsjs: unknown CommandBuffer");return t}function rt(n){let t=it(n);return {id:t.id,capacity:t.capacity,version:t.version}}function it(n){return l({id:n})}exports.createCommandBuffer=tt;exports.flush=nt;exports.withCommandBuffer=gt;//# sourceMappingURL=commands.cjs.map
2
2
  //# sourceMappingURL=commands.cjs.map