aiecsjs 0.1.3 → 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 +47 -2
  2. package/README.md +42 -12
  3. package/README_ZHTW.md +39 -10
  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,56 @@ 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.
52
+
53
+ ## [0.1.4] - 2026-05-28
54
+
55
+ Docs-only release. Adds a cross-package integration section pointing at the `aibridgejs` JSON envelope contract; no source code changes.
56
+
57
+ ### Documentation
58
+
59
+ - README and README_ZHTW gained an "Integration with aibridgejs" section explaining that `bridge.call` / `bridge.emit` enforce JSON-safe payloads and silently drop `Date`, `Map`, `Set`, and class instances. The correct shape for streaming world state across the bridge is `toJSON(world)` (or `serializeWorld(world)` wrapped in a JSON envelope) before emitting, not `getComponent(...)` direct. See [aiecsjs README · Integration with aibridgejs](README.md#integration-with-aibridgejs).
60
+ - Verified via the `aijs-integration-smoke` companion project: every named export from `aifsmjs@0.1.2`, `aibridgejs@0.1.3`, and `aiecsjs@0.1.3` can coexist in a single TypeScript module with zero identifier collisions under `tsc --noEmit --strict`.
16
61
 
17
62
  ## [0.1.3] - 2026-05-28
18
63
 
package/README.md CHANGED
@@ -5,12 +5,12 @@
5
5
  [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
6
6
  ![AI Generated](https://img.shields.io/badge/AI_Generated-Claude_Code_Opus_4.7_Max-blueviolet.svg)
7
7
  ![Status](https://img.shields.io/badge/status-experimental-orange.svg)
8
- ![Version](https://img.shields.io/badge/version-0.1.2-blue.svg)
8
+ ![Version](https://img.shields.io/badge/version-0.1.4-blue.svg)
9
9
  ![Types](https://img.shields.io/badge/types-TypeScript-3178c6.svg)
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
 
@@ -86,6 +86,27 @@ The core stays narrow on purpose. The following are explicit non-goals; reach fo
86
86
  - **Reactive value-predicate queries.** `enterQuery` / `exitQuery` fire on component-set membership change only. Component value mutations are not tracked.
87
87
  - **Prefab / entity inheritance / hierarchy.** `aiecsjs/relations` provides plain entity-to-entity references, not inheritance.
88
88
 
89
+ ## Integration with aibridgejs
90
+
91
+ If you stream world state across an [aibridgejs](https://www.npmjs.com/package/aibridgejs) bridge (iframe / Flutter InAppWebView), the bridge enforces a strict JSON envelope and silently drops `Date`, `Map`, `Set`, and class instances. AoS components from `defineObjectComponent(...)` can legally hold any of these; sending them as-is corrupts the payload on the host side.
92
+
93
+ Correct shape — serialise first, emit a plain object or byte array:
94
+
95
+ ```ts
96
+ import { toJSON } from 'aiecsjs/serialize'
97
+
98
+ const snap = toJSON(world)
99
+ await bridge.emit('world.snapshot', snap)
100
+ ```
101
+
102
+ Do NOT do — `getComponent` returns the live column view or the AoS instance with its prototype intact, which the bridge cannot transport:
103
+
104
+ ```ts
105
+ await bridge.emit('inv', getComponent(world, eid, Inventory))
106
+ ```
107
+
108
+ `serializeWorld(world)` (binary, `Uint8Array`) is also bridge-safe; wrap the bytes in a JSON envelope like `{ kind: 'binary', bytes: Array.from(snap) }`, or use a transferable channel when the host supports it.
109
+
89
110
  ## Install
90
111
 
91
112
  ```bash
@@ -264,7 +285,13 @@ const stopAdd = onAdd(world, Position, (e) => console.log('positioned', e))
264
285
  const stopRemove = onRemove(world, Player, (e) => console.log('un-playered', e))
265
286
  const stopSet = onSet(world, Health, (e, val) => console.log('health set', e, val))
266
287
 
267
- // 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:
268
295
  stopAdd()
269
296
  stopRemove()
270
297
  stopSet()
@@ -272,6 +299,8 @@ stopSet()
272
299
 
273
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.
274
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
+
275
304
  ### Command buffers — when and why
276
305
 
277
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:
@@ -314,7 +343,7 @@ addRelation(world, alice, ChildOf, parent)
314
343
  const parentOfAlice = getRelationTargets(world, alice, ChildOf)
315
344
  ```
316
345
 
317
- 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).
318
347
 
319
348
  ## API Reference
320
349
 
@@ -325,7 +354,8 @@ Full machine-readable surface in [`api.json`](./api.json). Stability flags in [`
325
354
  | Function | Signature | Stability |
326
355
  |---|---|---|
327
356
  | `createWorld` | `(options?: WorldOptions) => World` | stable |
328
- | `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 |
329
359
  | `resetWorld` | `(world: World) => void` | stable |
330
360
  | `getWorldSize` | `(world: World) => number` (alive count) | stable |
331
361
  | `getWorldCapacity` | `(world: World) => number` | stable |
@@ -350,8 +380,8 @@ type WorldOptions = {
350
380
  | `destroyEntity` | `(world: World, eid: EntityId) => void` | stable |
351
381
  | `entityExists` | `(world: World, eid: EntityId) => boolean` | stable |
352
382
  | `getEntityIndex` | `(eid: EntityId) => number` | stable |
353
- | `getEntityGeneration` | `(eid: EntityId) => number` | stable |
354
- | `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+** |
355
385
 
356
386
  ### Component — `aiecsjs`
357
387
 
@@ -408,10 +438,10 @@ const Types = { i8, u8, i16, u16, i32, u32, f32, f64, eid, bool } as const
408
438
 
409
439
  | Function | Signature | Stability |
410
440
  |---|---|---|
411
- | `observe` | `(world, q, event, handler) => () => void` | stable |
412
- | `onAdd` | `(world, comp, handler) => () => void` | stable |
413
- | `onRemove` | `(world, comp, handler) => () => void` | stable |
414
- | `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 |
415
445
 
416
446
  ### Serialization — `aiecsjs/serialize`
417
447
 
package/README_ZHTW.md CHANGED
@@ -5,12 +5,12 @@
5
5
  [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
6
6
  ![AI Generated](https://img.shields.io/badge/AI_Generated-Claude_Code_Opus_4.7_Max-blueviolet.svg)
7
7
  ![Status](https://img.shields.io/badge/status-experimental-orange.svg)
8
- ![Version](https://img.shields.io/badge/version-0.1.2-blue.svg)
8
+ ![Version](https://img.shields.io/badge/version-0.1.4-blue.svg)
9
9
  ![Types](https://img.shields.io/badge/types-TypeScript-3178c6.svg)
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
 
@@ -93,6 +93,27 @@ pipe(movement)(world, 1/60)
93
93
  - **反應式 value-predicate query。** `enterQuery` / `exitQuery` 只在元件集合 membership 變動時觸發。元件值變動不追蹤。
94
94
  - **Prefab / 實體繼承 / 階層。** `aiecsjs/relations` 提供純粹的實體對實體關聯,不是繼承。
95
95
 
96
+ ## 與 aibridgejs 整合
97
+
98
+ 若你透過 [aibridgejs](https://www.npmjs.com/package/aibridgejs) bridge(iframe / Flutter InAppWebView)傳遞 world 狀態,bridge 強制要求 JSON 信封並會無聲剝除 `Date`、`Map`、`Set` 與類別實例。`defineObjectComponent(...)` 的 AoS 元件可合法持有上述任何一種;直接 emit 過去會在 host 端被破壞。
99
+
100
+ 正確作法——先序列化,再 emit 純物件或 byte 陣列:
101
+
102
+ ```ts
103
+ import { toJSON } from 'aiecsjs/serialize'
104
+
105
+ const snap = toJSON(world)
106
+ await bridge.emit('world.snapshot', snap)
107
+ ```
108
+
109
+ 不要這樣做——`getComponent` 回傳的是 live column view 或保留 prototype 的 AoS 實例,bridge 無法傳輸:
110
+
111
+ ```ts
112
+ await bridge.emit('inv', getComponent(world, eid, Inventory))
113
+ ```
114
+
115
+ `serializeWorld(world)` 回傳的二進位 `Uint8Array` 也是 bridge-safe;把 byte 用 `{ kind: 'binary', bytes: Array.from(snap) }` 之類的 JSON 信封包起,或在 host 支援時改用 transferable channel。
116
+
96
117
  ## 安裝
97
118
 
98
119
  ```bash
@@ -272,7 +293,12 @@ const stopAdd = onAdd(world, Position, (e) => console.log('positioned', e))
272
293
  const stopRemove = onRemove(world, Player, (e) => console.log('un-playered', e))
273
294
  const stopSet = onSet(world, Health, (e, val) => console.log('health set', e, val))
274
295
 
275
- // 之後解除
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,兩種方式皆冪等可混用:
276
302
  stopAdd()
277
303
  stopRemove()
278
304
  stopSet()
@@ -280,6 +306,8 @@ stopSet()
280
306
 
281
307
  Observer 會在變動呼叫內同步觸發。用於需要在變動發生瞬間執行的副作用(除錯、複製)。對於要批次的 UI 更新,請改用反應式查詢。
282
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
+
283
311
  ### Command buffer:何時與為何
284
312
 
285
313
  黃金法則:**不要在正在迭代的實體上新增或移除元件。** 這樣做可能讓某些實體被跳過或被處理兩次,因為原型歸屬中途改變了。請用 command buffer 延後執行:
@@ -322,7 +350,7 @@ addRelation(world, alice, ChildOf, parent)
322
350
  const parentOfAlice = getRelationTargets(world, alice, ChildOf)
323
351
  ```
324
352
 
325
- 0.2 版會加入獨佔關係(只允許一個目標)、wildcard 查詢、以及關係圖的序列化。
353
+ Relations 穩定化 ─ 獨佔關係(只允許一個目標)、wildcard 查詢、以及關係圖的序列化 ─ 預計 **0.3+** 落地;0.2.0 保留 relations subpath 現有 experimental 形狀(無破壞變更)。
326
354
 
327
355
  ## API 參考
328
356
 
@@ -333,7 +361,8 @@ const parentOfAlice = getRelationTargets(world, alice, ChildOf)
333
361
  | 函式 | Signature | 穩定度 |
334
362
  |---|---|---|
335
363
  | `createWorld` | `(options?: WorldOptions) => World` | stable |
336
- | `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 移除 |
337
366
  | `resetWorld` | `(world: World) => void` | stable |
338
367
  | `getWorldSize` | `(world: World) => number`(存活實體數) | stable |
339
368
  | `getWorldCapacity` | `(world: World) => number` | stable |
@@ -416,10 +445,10 @@ const Types = { i8, u8, i16, u16, i32, u32, f32, f64, eid, bool } as const
416
445
 
417
446
  | 函式 | Signature | 穩定度 |
418
447
  |---|---|---|
419
- | `observe` | `(world, q, event, handler) => () => void` | stable |
420
- | `onAdd` | `(world, comp, handler) => () => void` | stable |
421
- | `onRemove` | `(world, comp, handler) => () => void` | stable |
422
- | `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,非反應式 |
423
452
 
424
453
  ### 序列化 — `aiecsjs/serialize`
425
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