@oasys/oecs 0.4.0 → 0.5.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.
- package/CHANGELOG.md +553 -0
- package/README.md +42 -31
- package/dist/core/ecs/access_check.d.cts +77 -0
- package/dist/core/ecs/access_check.d.ts +8 -8
- package/dist/core/ecs/access_check.d.ts.map +1 -1
- package/dist/core/ecs/archetype.d.cts +486 -0
- package/dist/core/ecs/archetype.d.ts +23 -11
- package/dist/core/ecs/archetype.d.ts.map +1 -1
- package/dist/core/ecs/archetype_graph.d.cts +96 -0
- package/dist/core/ecs/archetype_graph.d.ts +96 -0
- package/dist/core/ecs/archetype_graph.d.ts.map +1 -0
- package/dist/core/ecs/builtin_relations.d.cts +43 -0
- package/dist/core/ecs/builtin_relations.d.ts +8 -8
- package/dist/core/ecs/builtin_relations.d.ts.map +1 -1
- package/dist/core/ecs/command_log.d.cts +116 -0
- package/dist/core/ecs/command_log.d.ts +13 -10
- package/dist/core/ecs/command_log.d.ts.map +1 -1
- package/dist/core/ecs/component.d.cts +176 -0
- package/dist/core/ecs/component.d.ts +75 -5
- package/dist/core/ecs/component.d.ts.map +1 -1
- package/dist/core/ecs/compute_backend.d.cts +37 -0
- package/dist/core/ecs/compute_backend.d.ts +2 -2
- package/dist/core/ecs/debug_names.d.cts +7 -0
- package/dist/core/ecs/debug_names.d.ts +7 -0
- package/dist/core/ecs/debug_names.d.ts.map +1 -0
- package/dist/core/ecs/deferred_commands.d.cts +55 -0
- package/dist/core/ecs/deferred_commands.d.ts +55 -0
- package/dist/core/ecs/deferred_commands.d.ts.map +1 -0
- package/dist/core/ecs/dispatch_trace.d.cts +93 -0
- package/dist/core/ecs/dispatch_trace.d.ts +3 -3
- package/dist/core/ecs/ecs.d.cts +562 -0
- package/dist/core/ecs/ecs.d.ts +314 -285
- package/dist/core/ecs/ecs.d.ts.map +1 -1
- package/dist/core/ecs/ecs_memory.d.cts +179 -0
- package/dist/core/ecs/ecs_memory.d.ts +1 -1
- package/dist/core/ecs/entity.d.cts +34 -0
- package/dist/core/ecs/entity.d.ts +7 -1
- package/dist/core/ecs/entity.d.ts.map +1 -1
- package/dist/core/ecs/entity_allocator.d.cts +59 -0
- package/dist/core/ecs/entity_allocator.d.ts +59 -0
- package/dist/core/ecs/entity_allocator.d.ts.map +1 -0
- package/dist/core/ecs/event.d.cts +79 -0
- package/dist/core/ecs/event.d.ts +36 -11
- package/dist/core/ecs/event.d.ts.map +1 -1
- package/dist/core/ecs/event_registry.d.cts +22 -0
- package/dist/core/ecs/event_registry.d.ts +22 -0
- package/dist/core/ecs/event_registry.d.ts.map +1 -0
- package/dist/core/ecs/facades.d.cts +158 -0
- package/dist/core/ecs/facades.d.ts +158 -0
- package/dist/core/ecs/facades.d.ts.map +1 -0
- package/dist/core/ecs/frame_stepper.d.cts +58 -0
- package/dist/core/ecs/frame_stepper.d.ts +58 -0
- package/dist/core/ecs/frame_stepper.d.ts.map +1 -0
- package/dist/core/ecs/frame_trace.d.cts +133 -0
- package/dist/core/ecs/frame_trace.d.ts +5 -5
- package/dist/core/ecs/frame_trace.d.ts.map +1 -1
- package/dist/core/ecs/host_commands.d.cts +274 -0
- package/dist/core/ecs/host_commands.d.ts +53 -31
- package/dist/core/ecs/host_commands.d.ts.map +1 -1
- package/dist/core/ecs/index.d.cts +46 -0
- package/dist/core/ecs/index.d.ts +45 -40
- package/dist/core/ecs/index.d.ts.map +1 -1
- package/dist/core/ecs/observer.d.cts +173 -0
- package/dist/core/ecs/observer.d.ts +43 -12
- package/dist/core/ecs/observer.d.ts.map +1 -1
- package/dist/core/ecs/query.d.cts +718 -0
- package/dist/core/ecs/query.d.ts +251 -90
- package/dist/core/ecs/query.d.ts.map +1 -1
- package/dist/core/ecs/ref.d.cts +31 -0
- package/dist/core/ecs/ref.d.ts +3 -3
- package/dist/core/ecs/relation.d.cts +243 -0
- package/dist/core/ecs/relation.d.ts +26 -14
- package/dist/core/ecs/relation.d.ts.map +1 -1
- package/dist/core/ecs/relation_service.d.cts +235 -0
- package/dist/core/ecs/relation_service.d.ts +235 -0
- package/dist/core/ecs/relation_service.d.ts.map +1 -0
- package/dist/core/ecs/resource.d.cts +53 -0
- package/dist/core/ecs/resource.d.ts +25 -5
- package/dist/core/ecs/resource.d.ts.map +1 -1
- package/dist/core/ecs/resource_registry.d.cts +20 -0
- package/dist/core/ecs/resource_registry.d.ts +20 -0
- package/dist/core/ecs/resource_registry.d.ts.map +1 -0
- package/dist/core/ecs/resume.d.cts +85 -0
- package/dist/core/ecs/resume.d.ts +6 -6
- package/dist/core/ecs/resume.d.ts.map +1 -1
- package/dist/core/ecs/run_condition.d.cts +81 -0
- package/dist/core/ecs/run_condition.d.ts +15 -9
- package/dist/core/ecs/run_condition.d.ts.map +1 -1
- package/dist/core/ecs/schedule.d.cts +142 -0
- package/dist/core/ecs/schedule.d.ts +15 -6
- package/dist/core/ecs/schedule.d.ts.map +1 -1
- package/dist/core/ecs/snapshot_service.d.cts +94 -0
- package/dist/core/ecs/snapshot_service.d.ts +94 -0
- package/dist/core/ecs/snapshot_service.d.ts.map +1 -0
- package/dist/core/ecs/sparse_store.d.cts +110 -0
- package/dist/core/ecs/sparse_store.d.ts +5 -2
- package/dist/core/ecs/sparse_store.d.ts.map +1 -1
- package/dist/core/ecs/store.d.cts +950 -0
- package/dist/core/ecs/store.d.ts +143 -342
- package/dist/core/ecs/store.d.ts.map +1 -1
- package/dist/core/ecs/store_layout_listener.d.cts +23 -0
- package/dist/core/ecs/system.d.cts +250 -0
- package/dist/core/ecs/system.d.ts +138 -22
- package/dist/core/ecs/system.d.ts.map +1 -1
- package/dist/{utils/arrays.d.ts → core/ecs/utils/arrays.d.cts} +1 -0
- package/dist/core/ecs/utils/error.d.cts +61 -0
- package/dist/core/ecs/utils/error.d.ts +12 -2
- package/dist/core/ecs/utils/error.d.ts.map +1 -1
- package/dist/core/store/action_ring.d.cts +136 -0
- package/dist/core/store/allocator.d.cts +238 -0
- package/dist/core/store/allocator.d.ts.map +1 -1
- package/dist/core/store/buffer_backed_column.d.cts +69 -0
- package/dist/core/store/buffer_backed_column.d.ts +2 -2
- package/dist/core/store/column_store.d.cts +273 -0
- package/dist/core/store/column_store.d.ts +18 -10
- package/dist/core/store/column_store.d.ts.map +1 -1
- package/dist/core/store/command_dispatch.d.cts +52 -0
- package/dist/core/store/command_ring.d.cts +107 -0
- package/dist/core/store/descriptor.d.cts +80 -0
- package/dist/core/store/descriptor.d.ts +1 -1
- package/dist/core/store/descriptor.d.ts.map +1 -1
- package/dist/core/store/entity_index.d.cts +108 -0
- package/dist/core/store/event_ring.d.cts +95 -0
- package/dist/core/store/extend.d.cts +39 -0
- package/dist/core/store/extend.d.ts +3 -73
- package/dist/core/store/extend.d.ts.map +1 -1
- package/dist/core/store/grow.d.cts +34 -0
- package/dist/core/store/grow.d.ts +4 -9
- package/dist/core/store/grow.d.ts.map +1 -1
- package/dist/core/store/header.d.cts +64 -0
- package/dist/core/store/header.d.ts +1 -1
- package/dist/core/store/header.d.ts.map +1 -1
- package/dist/core/store/index.d.cts +17 -0
- package/dist/core/store/index.d.ts +16 -15
- package/dist/core/store/index.d.ts.map +1 -1
- package/dist/core/store/layout_ops.d.cts +152 -0
- package/dist/core/store/layout_ops.d.ts +152 -0
- package/dist/core/store/layout_ops.d.ts.map +1 -0
- package/dist/core/store/region_table.d.cts +74 -0
- package/dist/core/store/region_table.d.ts +1 -1
- package/dist/core/store/region_table.d.ts.map +1 -1
- package/dist/core/store/snapshot.d.cts +43 -0
- package/dist/core/store/snapshot.d.ts +2 -2
- package/dist/core/store/state_hash.d.cts +38 -0
- package/dist/core/store/state_hash.d.ts +1 -1
- package/dist/core/store/store_regions.d.cts +38 -0
- package/dist/core/store/store_regions.d.ts +1 -1
- package/dist/core/store/vendored_abi/abi.d.ts +43 -0
- package/dist/core/store/{__generated__ → vendored_abi}/abi.d.ts.map +1 -1
- package/dist/dev_flag.d.cts +17 -0
- package/dist/dev_flag.d.ts +17 -0
- package/dist/dev_flag.d.ts.map +1 -0
- package/dist/extensions/editor/editor.d.cts +170 -0
- package/dist/extensions/editor/editor.d.ts +43 -22
- package/dist/extensions/editor/editor.d.ts.map +1 -1
- package/dist/extensions/editor/field_handle.d.cts +40 -0
- package/dist/extensions/editor/field_handle.d.ts +8 -3
- package/dist/extensions/editor/field_handle.d.ts.map +1 -1
- package/dist/extensions/editor/index.cjs +1 -1
- package/dist/extensions/editor/index.d.cts +21 -0
- package/dist/extensions/editor/index.d.ts +2 -2
- package/dist/extensions/editor/index.js +116 -88
- package/dist/extensions/reactive/ecs_sync.d.cts +214 -0
- package/dist/extensions/reactive/ecs_sync.d.ts +26 -22
- package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -1
- package/dist/extensions/reactive/index.cjs +1 -1
- package/dist/extensions/reactive/index.d.cts +23 -0
- package/dist/extensions/reactive/index.d.ts +1 -1
- package/dist/extensions/reactive/index.js +106 -116
- package/dist/extensions/solid/index.cjs +1 -1
- package/dist/extensions/solid/index.d.cts +6 -0
- package/dist/extensions/solid/index.d.ts +2 -2
- package/dist/extensions/solid/index.js +8 -4
- package/dist/extensions/solid/kernel_solid.d.cts +45 -0
- package/dist/extensions/solid/kernel_solid.d.ts +6 -3
- package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
- package/dist/host_commands-BF8QMi3c.cjs +1 -0
- package/dist/host_commands-i4cAeyL5.js +2318 -0
- package/dist/index.cjs +1 -2
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +50 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4469 -5933
- package/dist/internal.cjs +2 -0
- package/dist/internal.d.cts +20 -0
- package/dist/internal.d.ts +20 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +141 -0
- package/dist/primitives.cjs +1 -1
- package/dist/primitives.d.cts +18 -0
- package/dist/primitives.d.ts +6 -6
- package/dist/primitives.js +24 -23
- package/dist/reactive/array.d.ts +24 -0
- package/dist/reactive/array.d.ts.map +1 -0
- package/dist/reactive/index.cjs +1 -0
- package/dist/reactive/index.d.cts +11 -0
- package/dist/reactive/index.d.ts +11 -0
- package/dist/reactive/index.d.ts.map +1 -0
- package/dist/reactive/index.js +18 -0
- package/dist/reactive/interop.d.cts +19 -0
- package/dist/{core/reactive → reactive}/interop.d.ts +1 -1
- package/dist/reactive/interop.d.ts.map +1 -0
- package/dist/reactive/kernel.d.cts +73 -0
- package/dist/{core/reactive → reactive}/kernel.d.ts +5 -3
- package/dist/reactive/kernel.d.ts.map +1 -0
- package/dist/reactive/map.d.ts +16 -0
- package/dist/reactive/map.d.ts.map +1 -0
- package/dist/reactive/shallow.d.cts +11 -0
- package/dist/reactive/shallow.d.ts +11 -0
- package/dist/reactive/shallow.d.ts.map +1 -0
- package/dist/reactive/struct.d.cts +10 -0
- package/dist/{core/reactive → reactive}/struct.d.ts +1 -1
- package/dist/reactive/struct.d.ts.map +1 -0
- package/dist/shallow-DQ8aG51_.cjs +1 -0
- package/dist/shallow-DfnAm9vG.js +150 -0
- package/dist/shared-BU1Cd40h.js +103 -0
- package/dist/shared-BymrGTyR.cjs +1 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.d.cts +22 -0
- package/dist/shared.d.ts +2 -2
- package/dist/shared.js +1 -1
- package/dist/topological_sort-BDvEyb9W.cjs +1 -0
- package/dist/topological_sort-DK6EjpWa.js +135 -0
- package/dist/type_primitives/assertions.d.cts +32 -0
- package/dist/type_primitives/assertions.d.ts +13 -1
- package/dist/type_primitives/assertions.d.ts.map +1 -1
- package/dist/type_primitives/binary_heap/binary_heap.d.cts +37 -0
- package/dist/type_primitives/bitset/bitset.d.cts +54 -0
- package/dist/type_primitives/brand.d.cts +23 -0
- package/dist/type_primitives/error.d.cts +16 -0
- package/dist/type_primitives/error.d.ts +4 -3
- package/dist/type_primitives/error.d.ts.map +1 -1
- package/dist/type_primitives/index.d.cts +13 -0
- package/dist/type_primitives/index.d.ts +9 -9
- package/dist/type_primitives/sparse_map/sparse_map.d.cts +30 -0
- package/dist/type_primitives/sparse_set/sparse_set.d.cts +28 -0
- package/dist/type_primitives/topological_sort/topological_sort.d.cts +29 -0
- package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +127 -0
- package/dist/typed_arrays-BcXcnzB4.js +258 -0
- package/dist/typed_arrays-D3NmwtCG.cjs +1 -0
- package/dist/utils/error.d.cts +6 -0
- package/dist/version.d.cts +10 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +102 -19
- package/dist/array-CxbPyiHO.cjs +0 -1
- package/dist/array-uFR7Q8fU.js +0 -132
- package/dist/core/reactive/array.d.ts.map +0 -1
- package/dist/core/reactive/index.cjs +0 -1
- package/dist/core/reactive/index.d.ts +0 -10
- package/dist/core/reactive/index.d.ts.map +0 -1
- package/dist/core/reactive/index.js +0 -17
- package/dist/core/reactive/interop.d.ts.map +0 -1
- package/dist/core/reactive/kernel.d.ts.map +0 -1
- package/dist/core/reactive/map.d.ts.map +0 -1
- package/dist/core/reactive/struct.d.ts.map +0 -1
- package/dist/log/console_sink.d.ts +0 -4
- package/dist/log/console_sink.d.ts.map +0 -1
- package/dist/log/index.d.ts +0 -3
- package/dist/log/index.d.ts.map +0 -1
- package/dist/log/logger.d.ts +0 -27
- package/dist/log/logger.d.ts.map +0 -1
- package/dist/shared-BXSZnxx4.cjs +0 -1
- package/dist/shared-C678TAPY.js +0 -99
- package/dist/topological_sort-DlRpSrxu.js +0 -391
- package/dist/topological_sort-WAT-VHb-.cjs +0 -1
- package/dist/utils/arrays.d.ts.map +0 -1
- package/dist/utils/constants.d.ts.map +0 -1
- /package/dist/{utils/constants.d.ts → core/ecs/utils/constants.d.cts} +0 -0
- /package/dist/core/store/{__generated__/abi.d.ts → vendored_abi/abi.d.cts} +0 -0
- /package/dist/{core/reactive/array.d.ts → reactive/array.d.cts} +0 -0
- /package/dist/{core/reactive/map.d.ts → reactive/map.d.cts} +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.5.0] — 2026-07-06
|
|
9
|
+
|
|
10
|
+
### Changed (breaking) — lifecycle & naming unification
|
|
11
|
+
|
|
12
|
+
One vocabulary across host, commands, and access declarations; the receiver now implies the
|
|
13
|
+
timing (host = immediate, `ctx.commands` = deferred). Hard renames, no deprecation aliases —
|
|
14
|
+
see [docs/MIGRATION-0.4-to-0.5.md](docs/MIGRATION-0.4-to-0.5.md) for the complete
|
|
15
|
+
rename/removal map:
|
|
16
|
+
|
|
17
|
+
| 0.4 | 0.5 |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `ecs.createEntity()` / `ecs.createEntity(template, overrides?)` | `ecs.spawn()` / `ecs.spawn(template, overrides?)` |
|
|
20
|
+
| `ecs.createEntities(template, count)` | `ecs.spawnMany(template, count, overrides?)` |
|
|
21
|
+
| `ecs.destroyEntity(e)` *(deferred)* | `ecs.despawn(e)` — **now immediate** |
|
|
22
|
+
| `ctx.createEntity()` | `ctx.commands.spawn()` |
|
|
23
|
+
| `ctx.destroyEntity(e)` | `ctx.commands.despawn(e)` |
|
|
24
|
+
| `ctx.addComponent(e, def, values?)` | `ctx.commands.add(e, def, values)` or `ctx.commands.add(e, def({ … }))` |
|
|
25
|
+
| `ctx.removeComponent(e, def)` | `ctx.commands.remove(e, def)` |
|
|
26
|
+
| `ctx.disable(e)` / `ctx.enable(e)` | `ctx.commands.disable(e)` / `ctx.commands.enable(e)` |
|
|
27
|
+
| `sourcesOf(def, tgt)` | `sourcesOf(tgt, def)` — matches `targetOf` / `targetsOf` |
|
|
28
|
+
| `query.count()` | `query.entityCount` (getter, beside `archetypeCount`) |
|
|
29
|
+
| `WorldRestoreError` / `WORLD_SNAPSHOT_VERSION` | `ECSRestoreError` / `ECS_SNAPSHOT_VERSION` |
|
|
30
|
+
| `DestroyEntityArg` (type) | `DespawnArg` |
|
|
31
|
+
|
|
32
|
+
- **Host `despawn` is immediate** — `ecs.despawn(e); ecs.isAlive(e)` is `false` on the next
|
|
33
|
+
line, closing the audit's M1 finding (host `addComponent` immediate but destroy buffered).
|
|
34
|
+
**Observer note:** like every immediate op, host `despawn` fires no *structural* observers —
|
|
35
|
+
`onRemove` no longer sees host-despawned entities (it did at 0.4, when host destroy was
|
|
36
|
+
deferred). Observer-driven consumers, including the `reactive-sync` map bridges, only see
|
|
37
|
+
despawns that go through `ctx.commands.despawn` or the host-command seam. (`onSet` is
|
|
38
|
+
receiver-blind — derived change detection sees host `setField` writes as always.)
|
|
39
|
+
- **Every immediate host structural mutator throws in dev when called from inside a system
|
|
40
|
+
body** — `despawn`, `addComponent`/`addComponents`, `removeComponent`/`removeComponents`,
|
|
41
|
+
`batchAddComponent`/`batchRemoveComponent`, `disable`/`enable` — each error pointing at its
|
|
42
|
+
`ctx.commands` equivalent. Mid-system these ops can move rows a running query is walking and
|
|
43
|
+
are invisible to observers; previously only `despawn` was guarded wholesale (the others were
|
|
44
|
+
caught only when they touched the archetype being iterated). Cross-world host mutation from
|
|
45
|
+
another world's system (#785) is unaffected — the guard is scoped to the mutated world.
|
|
46
|
+
- **The bare deferred duplicates on `ctx` are removed** — `ctx.addComponent`,
|
|
47
|
+
`ctx.removeComponent`, `ctx.disable`, `ctx.enable` join the already-removed
|
|
48
|
+
`ctx.createEntity` / `ctx.destroyEntity`. `ctx.commands` is now the *only* deferred surface,
|
|
49
|
+
completing the receiver-implies-timing rule with zero exceptions. `ctx.commands.add` gains
|
|
50
|
+
the explicit complete-values shape (`ctx.commands.add(e, Pos, { x: 0, y: 0 })`) the removed
|
|
51
|
+
`ctx.addComponent` carried, so compile-checked complete attaches survive the move.
|
|
52
|
+
`ctx.isDisabled` stays (immediate read), as do the immediate sparse/relation ops.
|
|
53
|
+
- **`sourcesOf` canonicalized to `(entity, def)`** on `ecs.relations` and `SystemContext` —
|
|
54
|
+
it was the one arg-order outlier on the relation surface (M3).
|
|
55
|
+
- **The package root is now a curated, explicit export list** — `export *` no longer flattens the
|
|
56
|
+
whole core barrel, so future barrel additions cannot silently widen the public API. A checked-in
|
|
57
|
+
public-API snapshot test makes any surface change an explicit diff in review.
|
|
58
|
+
- **Internal/tooling symbols moved to `@oasys/oecs/internal`** (explicitly **unstable — no semver
|
|
59
|
+
guarantees**): the packed-EntityID codec (`createEntityId`, `getEntityGeneration`, `MAX_INDEX`,
|
|
60
|
+
`MAX_GENERATION`, `MAX_LIVE_GENERATION`, `RETIRED_GENERATION`, `MAX_ENTITY_ID`), the SAB
|
|
61
|
+
command-ring transport (`HostCommandDispatcher`, `ring*Codec`, `HOST_COMMAND_PAYLOAD_BYTES`),
|
|
62
|
+
memory-sizing internals (`resolveECSMemory`, `DEFAULT_ECS_CAP_BYTES`, `BUDGET_*`), and the
|
|
63
|
+
dev-mode singletons (`accessCheck`, `dispatchTrace`). `getEntityIndex` stays at the root.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **`addComponent` bundle overload** — `ecs.addComponent(e, Pos({ x: 1 }))` accepts a bundle
|
|
68
|
+
with the usual zero-fill semantics (M2); the explicit `(e, def, values)` form stays
|
|
69
|
+
complete-values, so a typo'd or missing field is still a compile error.
|
|
70
|
+
- **`spawnMany` typed template + shared overrides** — bulk spawn takes the same typed
|
|
71
|
+
`Template<Defs>` as `spawn` plus one optional `TemplateOverrides<Defs>` object applied to
|
|
72
|
+
every row (contiguous batches use one `fill` per overridden column).
|
|
73
|
+
- **JSDoc `@example` on the core surface** — `registerComponent`, `spawn`, `addComponent`,
|
|
74
|
+
`query`, `registerSystem`, `startup`, `update`, `ctx.emit` / `ctx.read`,
|
|
75
|
+
`events.register`, `resources.register` now carry hover-visible examples (M23).
|
|
76
|
+
- **Component debug names** — `registerComponent(schema, { name: "Pos" })` (and the sparse
|
|
77
|
+
sibling) records a diagnostic label, so access-violation and liveness errors read
|
|
78
|
+
`'Pos' (component 5)` instead of leaving you to count registration order
|
|
79
|
+
(`ComponentRegisterOptions`).
|
|
80
|
+
- **Total probes + `tryGetField`** — `hasComponent` / `hasSparse` / `relations.has` now return
|
|
81
|
+
`false` for a dead entity instead of dev-throwing (a "has" probe is exactly the call made to
|
|
82
|
+
avoid dead entities); `ecs.tryGetField(e, def, field)` returns `undefined` for a dead entity or
|
|
83
|
+
missing component, and `ctx.tryGetField` mirrors it inside systems (declared-read checked).
|
|
84
|
+
- **Plural host mutators chain** — `addComponents`, `removeComponents`, `batchAddComponent`,
|
|
85
|
+
`batchRemoveComponent` return `this` (previously `void`), matching their singular siblings.
|
|
86
|
+
- **`Query.firstEntity()` / `Query.singleEntity()`** — singleton reads (player, camera) without a
|
|
87
|
+
hand-rolled `forEach` + capture; `singleEntity` dev-throws `QUERY_NOT_SINGLETON` on 0 or >1.
|
|
88
|
+
- **Host-side `ecs.refRead(def, e)`** — whole-component read-only view, parity with
|
|
89
|
+
`ctx.refRead`.
|
|
90
|
+
- **Run-condition combinators** — `not()` / `allOf()` / `anyOf()`, merging the operands' declared
|
|
91
|
+
read surfaces.
|
|
92
|
+
- **Editor change notification** — `editor.onChange(cb)` (fires on commit/undo/redo/clear) plus
|
|
93
|
+
`canUndo` / `canRedo` getters; no more per-frame `depths()` polling.
|
|
94
|
+
- **`using` support** — `ObserverHandle` implements `Symbol.dispose`.
|
|
95
|
+
- **Write-seam lifecycle** — `uninstallHostCommandSeam(world, queue)`,
|
|
96
|
+
`HostCommandQueue.clear()`, `HostCommandDispatcher.off(opCode)`,
|
|
97
|
+
`HostCommandRecorder.snapshotLog()` (stable deep copy).
|
|
98
|
+
- **`VERSION`** export and a `"./package.json"` export; `engines: { node: ">=20" }` and a README
|
|
99
|
+
runtime note (resizable `ArrayBuffer`).
|
|
100
|
+
- Root re-exports so failure modes are nameable without extra entry points:
|
|
101
|
+
`StoreRestoreError`, `SabUnavailableError`, `TypedArrayTag`; `/reactive` now exports `Eq` and
|
|
102
|
+
`shallow` (moved from `/reactive-sync`, which re-exports for compat); `signal()` gains the
|
|
103
|
+
zero-arg Solid-parity overload; `SingletonSyncOptions.eq`.
|
|
104
|
+
- **`FrameStepper`** — optional host-side driver over the authoritative `ecs.update(dt)`:
|
|
105
|
+
`play()`/`pause()`/`toggle()` on `requestAnimationFrame` (injectable `requestFrame`/`cancelFrame`
|
|
106
|
+
for tests and non-browser hosts), explicit `step()`/`stepFrames()` for debuggers, editors, and
|
|
107
|
+
rollback playback, and a `maxDt` clamp (default 0.25 s) so a resumed background tab doesn't feed
|
|
108
|
+
the whole suspension into the accumulator as one delta. Validation throws `INVALID_FRAME_STEP`.
|
|
109
|
+
- **`ObserverConfig.name`** — diagnostic label surfaced as the frame trace's
|
|
110
|
+
`observer_fired.observer` field (the role a system's `name` plays); observe-only, never affects
|
|
111
|
+
`stateHash` or dispatch order. Unnamed observers fall back to `observer(<component debug name>)`
|
|
112
|
+
when the component was registered with a name, else `observer(<cid>)`.
|
|
113
|
+
- **`ECSOptions.onWarn`** — injectable sink for dev-mode engine diagnostics (currently the
|
|
114
|
+
schedule's dropped-ordering-edge warning and the `ECSOptions` unknown-key warning),
|
|
115
|
+
defaulting to `console.warn`. Replaces the internal `src/log` singleton, which is deleted.
|
|
116
|
+
- **Editor `fieldHandle` `read` thunk is optional** — defaults to `Editor.committedField`.
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- **Host iteration guard (`STRUCTURAL_DURING_ITERATION`)** — with host `despawn` now immediate,
|
|
121
|
+
a host-side `forEach`/`eachChunk` callback that despawned (or transitioned/toggled) an entity of
|
|
122
|
+
the archetype it was visiting silently skipped entities via the row swap-remove. Row-removing
|
|
123
|
+
ops on an archetype a live dense walk is standing in now throw in dev, *before* any mutation
|
|
124
|
+
lands (the transition path checks ahead of the destination append, so no dual-residency
|
|
125
|
+
half-state). Collect ids during the walk and mutate after it. Mutating archetypes the walk is
|
|
126
|
+
*not* currently visiting stays legal — the #431 fresh-snapshot machinery still covers those.
|
|
127
|
+
- **Cross-world despawn false positive** — `worldB.despawn(e)` from inside world A's system no
|
|
128
|
+
longer trips the in-system despawn guard (the accessCheck span is process-global; the guard now
|
|
129
|
+
also requires *this* world to be mid-schedule). Driving a second world from a system (#785)
|
|
130
|
+
mutates it host-style, which is safe — B is not iterating. Unnamed systems in the guard message
|
|
131
|
+
now render as `system_<id>` instead of `'?'`.
|
|
132
|
+
- **Frame trace records every deferred command (ADR-0030)** — the removed bare `ctx.*` deferred
|
|
133
|
+
forms bypassed the `commandQueued` trace hook, so host-command-seam adds/removes/toggles (and
|
|
134
|
+
any system using the bare forms) were invisible to an attached `FrameTraceSink` while their
|
|
135
|
+
spawns/despawns were visible. With `ctx.commands` as the only deferred surface every queued
|
|
136
|
+
command is traced, and `ctx.commands.spawn` now also traces each bundle attach it queues
|
|
137
|
+
(previously only the spawn itself).
|
|
138
|
+
- **Stale deferred-attach docs** — `host_commands.ts` / the host-write-seam page claimed the
|
|
139
|
+
deferred add path does not zero-fill omitted fields (NaN readback); every attach path
|
|
140
|
+
zero-fills since #716 (`writeFields`'s `?? 0`). The complete-values requirement on
|
|
141
|
+
`SpawnEntry` is documented as what it is — explicit intent in a reified, replayable record —
|
|
142
|
+
and the observer docs now scope "immediate ops fire no observers" to *structural* observers
|
|
143
|
+
(`onSet` is derived change detection and sees host `setField` writes).
|
|
144
|
+
- **`ecs.refRead` / `ctx.ref` / `ctx.refRead` on a missing component or tag def** — threw a raw
|
|
145
|
+
`TypeError` from the ref internals; now a dev `ECSError` (`COMPONENT_NOT_REGISTERED`) naming the
|
|
146
|
+
op and component, matching `getField`. Host `refRead`'s docstring now states the single-
|
|
147
|
+
expression lifetime rule (any immediate structural mutation can row-swap under a held ref).
|
|
148
|
+
- **Editor: aborted transactions no longer poison undo** — `transaction(tx => …)` staged its
|
|
149
|
+
`setField` shadow writes into the editor's shared map at build time, so a build callback that
|
|
150
|
+
threw left phantom pending values behind and seeded the *next* edit's undo inverse with a value
|
|
151
|
+
the world never held. Staging is now transaction-local and merges only on commit.
|
|
152
|
+
- **Editor: `pendingField` self-resolves for dead slots** — a shadow entry for a despawned entity
|
|
153
|
+
(or removed component) echoed its stale value forever and leaked; the reconcile-on-read now
|
|
154
|
+
prunes it and returns `undefined`.
|
|
155
|
+
- **JSR/Deno consumers no longer break on the `__DEV__` global** — shipped source now reads a
|
|
156
|
+
guarded `DEV` flag (`src/dev_flag.ts`) that constant-folds in the npm bundle and defaults to
|
|
157
|
+
dev-on for raw-source consumers (`globalThis.__DEV__ = false` opts out).
|
|
158
|
+
- **Error experience** — every `ENTITY_NOT_ALIVE` names the operation and decodes the packed id
|
|
159
|
+
(index + generation, with context); system access violations use the new `ACCESS_UNDECLARED`
|
|
160
|
+
category instead of overloading `*_NOT_REGISTERED`; resource/event "not registered" messages
|
|
161
|
+
name the key and hint the registration call; messages no longer reference pre-0.4 snake_case
|
|
162
|
+
option names or private tracker issue numbers.
|
|
163
|
+
- **Packaging** — per-entry `.d.cts` and explicit-extension declaration specifiers
|
|
164
|
+
(`attw --pack` fully green: node10/node16/bundler across all eight entry points, was
|
|
165
|
+
masquerading + resolution errors); `typesVersions` for `moduleResolution: node10` subpaths; npm
|
|
166
|
+
tarball ships `CHANGELOG.md`; `@internal` editor internals no longer leak into published types.
|
|
167
|
+
- **Type-level closures** — `EventShape<S>` homomorphic bound (interface-declared event schemas
|
|
168
|
+
now accepted); `RelationOptions` is a union so `{ exclusive: true, multi: true }` is a compile
|
|
169
|
+
error; `ResourceKey`'s phantom is a unique symbol (no `.__phantom` in autocomplete);
|
|
170
|
+
`pairsOf` / `sourcesOfAny` return readonly tuples; `SystemConfig.fn` optional when
|
|
171
|
+
`backendHandle` is present.
|
|
172
|
+
- Dev-mode diagnostics: ownerless `computed()` / `onCleanup()` warn (kernel); ECSOptions warns on
|
|
173
|
+
unknown keys; `runIfResourceEq` warns on object-valued `expected` (reference-identity `===`);
|
|
174
|
+
`runEveryNTicks` validation throws `ECSError` (`INVALID_RUN_CONDITION`).
|
|
175
|
+
- **Docs standardized on the `ecs` receiver** — README, GETTING_STARTED, BEST_PRACTICES, the
|
|
176
|
+
api reference, and every in-source JSDoc example now spell `const ecs = new ECS()`
|
|
177
|
+
(M22; with the `World*` names renamed to `ECS*`, "world" survives only as prose). The
|
|
178
|
+
host-write-seam docs now explain *why* `queue.spawn` takes complete-value `spawnEntry`s
|
|
179
|
+
rather than zero-filling bundles (M4: commands are a reified, replayable record — complete
|
|
180
|
+
values are explicit intent legible to replay, not a correctness need; the deferred add path
|
|
181
|
+
zero-fills omitted fields since #716).
|
|
182
|
+
- **JSR publish no longer ships `__tests__` helper files** (`casing_codemod.ts`,
|
|
183
|
+
`test_helpers.ts` — including a `node:fs` import subject to JSR type-checking).
|
|
184
|
+
|
|
185
|
+
### Changed (breaking) — type-level & facade surface
|
|
186
|
+
|
|
187
|
+
- **Compile-time typestate across the system, query, relation, and key seams.** The config-form
|
|
188
|
+
`registerSystem` now infers your access declarations as literal types and hands `fn`/`onAdded` a
|
|
189
|
+
`SystemContext<DeclaredAccess<…>>` narrowed to exactly the declared surface — undeclared access
|
|
190
|
+
is a compile error naming the missing declaration, with the dev-mode runtime check remaining as
|
|
191
|
+
the backstop for dynamic values. Query columns are typed by the query's terms
|
|
192
|
+
(`ChunkColumns<Defs>` / `ArchetypeView<Defs>`; `.and(...)` extends the term set), relation
|
|
193
|
+
handles carry their cardinality (`RelationDef<"exclusive">` vs `RelationDef<"multi">` — the
|
|
194
|
+
exclusive-only traversal surfaces reject a multi handle at compile time), and
|
|
195
|
+
`ResourceKey`/`EventKey`/`EventDef` are invariant so a key can no longer widen through
|
|
196
|
+
`unknown`. A checked-in type battery (`typing_assertions.ts`) pins every rule.
|
|
197
|
+
- **Grouped facades: `ecs.relations`, `ecs.events`, `ecs.resources`, `ecs.snapshots`.** Cohesive
|
|
198
|
+
secondary surfaces move off the flat namespace onto narrow typed facades —
|
|
199
|
+
`ecs.relations.add(child, ChildOf, parent)`, `ecs.events.emit(Damage, {...})`,
|
|
200
|
+
`ecs.resources.get(Time)`, `ecs.snapshots.capture()`. The facades mirror the typestate
|
|
201
|
+
surface exactly (cardinality-stamped `relations.register`, exclusive-only traversal). Hot-path
|
|
202
|
+
API (component ops, queries, spawn/destroy, sparse ops) stays flat by design. Facade classes
|
|
203
|
+
are exported type-only; the runtime export list is unchanged.
|
|
204
|
+
- **Value arguments are schema-checked at compile time across every attach seam.** Tag defs
|
|
205
|
+
reject value objects (`Frozen({ x: 1 })` no longer compiles — tags carry no data);
|
|
206
|
+
`addComponents` takes schema-checked entries (`TemplateEntries<Defs>`), so a misspelled or
|
|
207
|
+
cross-component field key is a compile error instead of a silent zero-fill; host-seam
|
|
208
|
+
`queue.spawn` entries (`SpawnEntries<Defs>`) are checked complete against each def's own
|
|
209
|
+
schema (`ValuesArg` / `CompleteFieldValues` exported); and `events.register` requires the
|
|
210
|
+
field list to cover the event schema (`EventFieldsCover`) — a partial list silently dropped
|
|
211
|
+
columns and read back `undefined` at runtime. Smaller closures in the same vein: `observe`
|
|
212
|
+
accepts any `ComponentHandle`, `NoInfer` pins key-typed value params (`events.emit`,
|
|
213
|
+
resources), and reactive-sync's `JoinReader.field` is constrained to the join's component
|
|
214
|
+
set.
|
|
215
|
+
|
|
216
|
+
### Removed (breaking)
|
|
217
|
+
|
|
218
|
+
- The 29 flat forms the new facades replace (`registerRelation`/`addRelation`/`targetOf`/…,
|
|
219
|
+
`registerEvent`/`registerSignal`/`emit`/`read`, `registerResource`/`resource`/`setResource`/
|
|
220
|
+
`removeResource`/`hasResource`, `snapshot`/`restoreInto`/`snapshotSparse`/`restoreSparse`/
|
|
221
|
+
`stateHash`/`deterministic`, `relationCount`/`compactRelations`). Each maps 1:1 onto its
|
|
222
|
+
grouped replacement — `ecs.relations.add(...)`, `ecs.events.emit(...)`, `ecs.resources.get(...)`,
|
|
223
|
+
`ecs.snapshots.capture()` (was `snapshot()`) / `ecs.snapshots.restore(...)` (was
|
|
224
|
+
`restoreInto(...)`), `ecs.relations.count` (was `relationCount`), `ecs.relations.compact()`
|
|
225
|
+
(was `compactRelations()`). System-side `ctx.*` and all `Store`-level methods are unchanged.
|
|
226
|
+
|
|
227
|
+
### Changed (internal)
|
|
228
|
+
|
|
229
|
+
- **`Store` decomposed into seven focused collaborators** (RelationService, EventRegistry +
|
|
230
|
+
ResourceRegistry, EntityAllocator, DeferredCommandBuffer, SnapshotService, ArchetypeGraph) with
|
|
231
|
+
`Store` as the coordinator; the hot-path extractions were A/B-benchmarked against
|
|
232
|
+
identical-code controls with no regression. The `ECS` facade's pure delegations now live in a
|
|
233
|
+
marker-delimited pass-through band whose logic-free invariant is enforced by an AST guard test.
|
|
234
|
+
- Typed per-consumer host seams (`ObserverHost`, `QueryHost`) replace underscore-convention
|
|
235
|
+
reach-through on `Store`; `QueryCache` now owns all 12 query-resolution cache maps.
|
|
236
|
+
- Store layer consolidation: one strategy-parameterized factory behind
|
|
237
|
+
`growableSabAllocator` / `heapArraybufferAllocator`; a typed `isColumnStoreInternal` guard
|
|
238
|
+
replaces six structural casts; grow/extend's ~200 duplicated lines moved to a shared
|
|
239
|
+
`layout_ops.ts` (bit-identical layouts pinned by a golden differential test across the
|
|
240
|
+
full allocator matrix).
|
|
241
|
+
- `core/reactive` moved to `src/reactive` (the published `./reactive` subpath is unchanged);
|
|
242
|
+
`__generated__/abi.ts` renamed to `vendored_abi/abi.ts` (it is a hand-maintained snapshot,
|
|
243
|
+
not generated output).
|
|
244
|
+
- Deleted orphaned duplicate `src/utils/{arrays,constants}.ts`; renamed the custom `TypeError`
|
|
245
|
+
(shadowed the ECMAScript global) to `AssertionError`; retired the 246-line casing codemod +
|
|
246
|
+
guard test (the 0.4 rename has converged).
|
|
247
|
+
|
|
248
|
+
## [0.4.0] — 2026-06-24
|
|
249
|
+
|
|
250
|
+
Major release. oecs is **re-derived from the upstream oasys engine ECS** — its modern descendant — and
|
|
251
|
+
gains whole subsystems while staying pure-TS and zero-dependency by default. The public API moves to
|
|
252
|
+
the engine's surface, so **every consumer touches breaking changes** — chiefly a global
|
|
253
|
+
`snake_case` → `camelCase` rename. See [docs/MIGRATION-0.3-to-0.4.md](docs/MIGRATION-0.3-to-0.4.md).
|
|
254
|
+
|
|
255
|
+
### Changed (breaking)
|
|
256
|
+
|
|
257
|
+
- **The entire public API is now `camelCase`.** Every method, property, parameter, and field renamed
|
|
258
|
+
from `snake_case` (`create_entity` → `createEntity`, `add_component` → `addComponent`, `get_field` →
|
|
259
|
+
`getField`, `is_alive` → `isAlive`, `register_system` → `registerSystem`, …). Types and handles stay
|
|
260
|
+
PascalCase and SCREAMING_SNAKE constants are unchanged. A `vitest` casing guard prevents regressions.
|
|
261
|
+
- **Renamed query/context verbs.** `QueryBuilder.every` → `with`; `query.not` → `without`;
|
|
262
|
+
`query.any_of` → `anyOf`; `query.for_each` → `forEach`; `archetype.get_column` → `getColumnRead`;
|
|
263
|
+
`event_key` / `signal_key` / `resource_key` → `eventKey` / `signalKey` / `resourceKey`;
|
|
264
|
+
`is_ecs_error` → `isEcsError`; `destroy_entity_deferred` → `destroyEntity` (still deferred).
|
|
265
|
+
- **Ref mutability flipped on the unsuffixed name.** `ctx.ref` is now the **mutable** default (was
|
|
266
|
+
read-only in 0.3); the read-only variant is `ctx.refRead` (was `ctx.ref_mut` for the mutable one).
|
|
267
|
+
Same rule for columns: mutable `getColumn` (internal) vs read-only `getColumnRead`.
|
|
268
|
+
- **`WorldOptions` → `ECSOptions`; `fixed_timestep` → `fixedTimestep`.**
|
|
269
|
+
- **`initial_capacity` removed** — replaced by the `memory` surface (`memory: { budget }` /
|
|
270
|
+
`{ maxBytes }` / `{ columnCapacity }` pin / `{ shared }` / `{ wasm }` / `{ allocator }`). Passing the
|
|
271
|
+
old option keys throws at construction, pointing at `memory`.
|
|
272
|
+
- **Component-touching systems must declare `reads` / `writes`.** A new `__DEV__` access checker
|
|
273
|
+
(tree-shaken from production) validates every column / ref / field / resource access against a
|
|
274
|
+
system's declared surface. The bare `(ctx, dt)` and `(q, ctx, dt)` + query-builder `registerSystem`
|
|
275
|
+
overloads declare no access, so a system that touches ECS data through them throws in dev — move it
|
|
276
|
+
to the config form (`registerSystem({ reads, writes, fn })`). `exclusive: true` systems bypass the
|
|
277
|
+
checker. A registration-time lint (`QUERY_ACCESS_UNDECLARED`) additionally checks any declared
|
|
278
|
+
`queries ⊆ reads ∪ writes`.
|
|
279
|
+
- **`removeComponents` takes an array, not varargs** (`removeComponents(e, [A, B])`);
|
|
280
|
+
`batchAddComponent` / `batchRemoveComponent` key on `ArchetypeID` instead of an `Archetype` object.
|
|
281
|
+
- **Event schema shape.** `eventKey`'s type parameter is now a field → value-type record
|
|
282
|
+
(`eventKey<{ target: EntityID; amount: number }>("Damage")`) rather than a tuple of field names, so
|
|
283
|
+
branded fields round-trip through `emit` / `read`. `registerEvent(key, [...fieldNames])` unchanged
|
|
284
|
+
otherwise.
|
|
285
|
+
|
|
286
|
+
### Added
|
|
287
|
+
|
|
288
|
+
- **Two storage profiles over one backing-neutral `ColumnStore`.** Default is pure-TS **heap** (a plain
|
|
289
|
+
resizable `ArrayBuffer`) — no `SharedArrayBuffer`, no cross-origin isolation. Opt-in
|
|
290
|
+
`@oasys/oecs/shared` (`memory: { shared: {} }`) uses a `SharedArrayBuffer` for worker offload / a WASM
|
|
291
|
+
compute backend. Same code path; identical state hash.
|
|
292
|
+
- **Determinism** (opt-in `deterministic: true`): a state hash over column bytes + `snapshot()` /
|
|
293
|
+
`restoreInto()` (and `snapshotSparse` / `restoreSparse`), **backing-agnostic** — a heap world and a
|
|
294
|
+
shared world with identical history agree. `WorldRestoreError` / `SparseRestoreError` fail closed
|
|
295
|
+
before overwriting live backing.
|
|
296
|
+
- **Observers** — `world.observe(def, { onAdd, onRemove, onSet, onDisable, onEnable })`, structural +
|
|
297
|
+
per-entity.
|
|
298
|
+
- **Relations** — `(relation, target)` pairs, `ChildOf` / `IsA` presets (`registerChildOf` /
|
|
299
|
+
`registerIsA`), `(R,*)` / `(*,T)` wildcard queries (`withRelation`, `forEachRelatedTo`,
|
|
300
|
+
`ANY_RELATION`), hierarchy queries (`query.hierarchy`), traversal (`ancestorsOf` / `rootOf` /
|
|
301
|
+
`cascadeOf`), and on-delete cleanup policies.
|
|
302
|
+
- **Sparse component storage** (`registerSparseComponent` / `addSparse` / `query.withSparse`),
|
|
303
|
+
**run conditions / system sets** (`systemSet` + `configureSet`; `runIfResourceEq` / `runEveryNTicks`
|
|
304
|
+
/ `runIfAnyMatch`), **entity enable/disable** (row-partitioned; `disable` / `enable` /
|
|
305
|
+
`includeDisabled`), and **templates** (`world.template([...])` + `createEntity(template, overrides)`
|
|
306
|
+
/ `createEntities(template, count)` for zero-transition spawns).
|
|
307
|
+
- **Typed host→ECS write seam** — `installHostCommandSeam(world)` + `applyHostCommand` + a
|
|
308
|
+
`HostCommandQueue` drained by a blessed `exclusive` apply system; a cross-thread ring transport
|
|
309
|
+
(`HostCommandDispatcher`); record/replay (`HostCommandRecorder`, `replayCommandLog`,
|
|
310
|
+
`serializeCommandLog`); and an undo/redo + field-handle layer at `@oasys/oecs/editor`.
|
|
311
|
+
- **Frame trace** — `world.setTrace(sink)` + `FrameTraceRecorder` emit a structured per-frame event
|
|
312
|
+
stream (`__DEV__`-gated). **Compute backend seam** — `world.attachBackend(backend)` runs a system's
|
|
313
|
+
body on a compiled backend instead of its TS closure.
|
|
314
|
+
- **Reactive UI seam (optional):** zero-dependency kernel at `@oasys/oecs/reactive`; ECS→reactive
|
|
315
|
+
bridge at `@oasys/oecs/reactive-sync` (publish-only-dirty, O(changed)); SolidJS adapter at
|
|
316
|
+
`@oasys/oecs/solid` with `solid-js` as an **optional** peer dependency.
|
|
317
|
+
- **`memory` sizing surface** on the constructor: `budget` (by expected `entities`) / `maxBytes` /
|
|
318
|
+
`columnCapacity` / `shared` / `wasm` / `allocator` arms; `resolveECSMemory(...)` exported to inspect
|
|
319
|
+
what an intent resolves to.
|
|
320
|
+
- **Hot-path iteration ergonomics:**
|
|
321
|
+
- **`query.eachChunk((cols, count) => …)`** — the mutable per-archetype iterator. `cols.mut(def)` /
|
|
322
|
+
`cols.read(def)` resolve a whole component's field columns at once into a destructurable group
|
|
323
|
+
(`const { x, y } = cols.mut(Pos)`), stamping the change tick once inside `mut` and handing back
|
|
324
|
+
`count` (= `entityCount`). The only mutable column accessor reachable through iteration (the
|
|
325
|
+
`ArchetypeView` from `forEach` stays read-only). Honours `includeDisabled()`; dense-only like `forEach`.
|
|
326
|
+
- **`ctx.commands`** — a Bevy-`Commands`-style facade namespacing the **deferred** structural ops
|
|
327
|
+
(`spawn` / `add` / `remove` / `despawn` / `disable` / `enable`), unambiguously deferred vs the
|
|
328
|
+
immediate `world.addComponent`.
|
|
329
|
+
- **Callable bundles** — `bundle(def, values)` pairs a def with field values (omitted fields
|
|
330
|
+
zero-fill); `world.spawnBundle(...)` (immediate) and `ctx.commands.spawn` / `.add` (deferred)
|
|
331
|
+
accept a `bundle(...)` or a bare def (tag / all-zero), unifying the attach shapes.
|
|
332
|
+
- **`ctx.updateField` / `ctx.markChanged`**, and optional-component queries (`query.optional(...)` +
|
|
333
|
+
`getOptionalColumnRead`).
|
|
334
|
+
- **Composable change-detection queries** — `query.changed(...)` returns a `ChangedQuery` that now
|
|
335
|
+
mirrors the dense query verbs (`and` / `without` / `anyOf` / `optional`), so
|
|
336
|
+
`q.changed(Pos).without(Dead)` works (refining *after* `changed()`, previously a dead end).
|
|
337
|
+
- **New public exports** — entity-ID codec (`createEntityId` / `getEntityIndex` / `getEntityGeneration`
|
|
338
|
+
+ `MAX_*` bounds) for snapshot/replication decode; the error taxonomy (`ECSError`, `ECS_ERROR`,
|
|
339
|
+
`isEcsError`) for catch-and-branch; and `@oasys/oecs/primitives` (`BitSet`, `SparseSet`, `SparseMap`,
|
|
340
|
+
growable typed arrays, `BinaryHeap`, `topologicalSort`).
|
|
341
|
+
|
|
342
|
+
### Packaging
|
|
343
|
+
|
|
344
|
+
- **Multi-entry build** → `dist/` emits ESM + CJS + `.d.ts` for every subpath (`.`, `/primitives`,
|
|
345
|
+
`/shared`, `/reactive`, `/reactive-sync`, `/editor`, `/solid`); `sideEffects:false` + tree-shaking
|
|
346
|
+
keep core consumers from pulling SAB / Solid. `solid-js` is an optional peer dependency. `jsr.json`
|
|
347
|
+
exports updated.
|
|
348
|
+
|
|
349
|
+
## [0.3.3] — 2026-04-30
|
|
350
|
+
|
|
351
|
+
Release-process and packaging hygiene. No runtime changes.
|
|
352
|
+
|
|
353
|
+
### Changed
|
|
354
|
+
|
|
355
|
+
- **JSR bundle slimmed.** `.github/` and `docs/` are now excluded from the published JSR package. Consumers download less; build/CI artefacts stay on GitHub.
|
|
356
|
+
- **Tag-driven publish workflow.** `.github/workflows/publish.yml` now triggers on `v*` tag pushes instead of every push to `main`, and creates a GitHub Release alongside the JSR publish. Cuts a release by tagging.
|
|
357
|
+
|
|
358
|
+
## [0.3.2] — 2026-04-30
|
|
359
|
+
|
|
360
|
+
Documentation-only release. No runtime changes.
|
|
361
|
+
|
|
362
|
+
### Added
|
|
363
|
+
|
|
364
|
+
- **Module overview on `src/index.ts`.** A `@module` block now renders as the JSR Overview tab.
|
|
365
|
+
- **JSDoc on the full public surface.** `ECS` and its public methods, `Query` / `QueryBuilder` / `SystemContext` / `ChangedQuery`, all type aliases and interfaces, the event/resource key minters, and the `SCHEDULE` phases are now documented in-source.
|
|
366
|
+
- **`@internal` tags on internal-but-public TS members** (e.g. `_resolve_query`, `Query._include`, `SystemContext.store`) so JSR hides them from the rendered docs.
|
|
367
|
+
|
|
368
|
+
## [0.3.1] — 2026-04-23
|
|
369
|
+
|
|
370
|
+
Performance-only patch release. Two targeted allocation-elimination changes on hot paths; no API changes; full 466-test suite unchanged.
|
|
371
|
+
|
|
372
|
+
### Performance
|
|
373
|
+
|
|
374
|
+
- **Cache multi-component transition maps on `Archetype`.** `add_components` / `remove_components` on already-populated entities previously allocated a fresh `Int16Array` per call via `build_transition_map`. A per-archetype `batch_transition_maps: Map<ArchetypeID, Int16Array>` now caches the map on first use. Single-component paths unchanged. Measured: **+12–15%** throughput on `add_components` (already-populated) at 10k / 100k / 1M; **−35–42%** peak heap and **−49–61%** peak RSS on the same workload. ([#9](https://github.com/oasys-works/oecs/pull/9))
|
|
375
|
+
- **Per-Query composition cache for single-component composition shapes.** `q.and(X)`, `q.not(X)`, `q.any_of(X)`, and `q.changed(X)` previously allocated a BitSet copy, a defs slice (and, for `.changed`, a new `ChangedQuery`) on every call, even though the resolver already cached the resulting `Query` object. Single-component calls now short-circuit through a per-parent-`Query` Map and skip the allocation path entirely. Multi-component compositions fall through unchanged. Measured: **~6×** throughput on a 4-shape compose loop at 10k / 100k / 1M; **−40–56%** peak heap and **essentially zero RSS growth** during the workload. ([#10](https://github.com/oasys-works/oecs/pull/10))
|
|
376
|
+
|
|
377
|
+
## [0.3.0] — 2026-04-21
|
|
378
|
+
|
|
379
|
+
A substantial release focused on change detection, stricter component-access
|
|
380
|
+
typing, and a simpler key-based API for events and resources. Several public
|
|
381
|
+
entry points change shape; see the migration notes under *Breaking changes*.
|
|
382
|
+
|
|
383
|
+
### Added
|
|
384
|
+
|
|
385
|
+
#### Change detection
|
|
386
|
+
|
|
387
|
+
- Frame-based tick counter on the world. `ECS` now holds a `_tick` that
|
|
388
|
+
advances once per `update()`. Systems can see it via `ctx.world_tick`,
|
|
389
|
+
and each `SystemContext` receives `last_run_tick` — the tick at which that
|
|
390
|
+
system last executed.
|
|
391
|
+
- Per-component change ticks on archetypes. Each archetype tracks
|
|
392
|
+
`_changed_tick[component_id]` — the tick at which any entity in that
|
|
393
|
+
archetype last had the component mutated. Maintained automatically by
|
|
394
|
+
`write_fields`, `write_fields_positional`, `copy_shared_from`,
|
|
395
|
+
`move_entity_from`, and `bulk_move_all_from`, all of which now accept a
|
|
396
|
+
`tick` parameter.
|
|
397
|
+
- `ChangedQuery<Defs>` — a new query variant, produced by `query.changed(...)`,
|
|
398
|
+
that restricts iteration to archetypes whose tracked components were
|
|
399
|
+
modified after `last_run_tick`. Validates at construction that the named
|
|
400
|
+
components are part of the parent query's include set.
|
|
401
|
+
|
|
402
|
+
#### Readonly component views
|
|
403
|
+
|
|
404
|
+
- `ReadonlyColumn<T>` and `ReadonlyUint32Array` — compile-time readonly views
|
|
405
|
+
of typed-array columns. Returned by `archetype.get_column()` and the new
|
|
406
|
+
`archetype.entity_ids` getter. Prevents accidental indexed writes at the
|
|
407
|
+
type level; zero runtime cost.
|
|
408
|
+
- `ReadonlyComponentRef<S>` — readonly variant of `ComponentRef`. Returned by
|
|
409
|
+
`query.ref(...)`. Use it when you only need to read component fields.
|
|
410
|
+
- `archetype.get_column_mut(def, field, tick)` — explicit mutable column
|
|
411
|
+
accessor. Writes through `get_column_mut` update `_changed_tick`.
|
|
412
|
+
- `query.ref_mut(...)` — mutable sibling of `ref()`. Returns a `ComponentRef`
|
|
413
|
+
and records the component as changed for the current tick.
|
|
414
|
+
|
|
415
|
+
#### Key-based Event API
|
|
416
|
+
|
|
417
|
+
- `EventKey<F>` — symbol-typed key that carries the event's field schema as
|
|
418
|
+
a phantom type.
|
|
419
|
+
- `event_key<F>(name)` / `signal_key(name)` — factories for module-scope
|
|
420
|
+
event keys. `signal_key` is a convenience wrapper for zero-field events.
|
|
421
|
+
|
|
422
|
+
#### Key-based Resource API
|
|
423
|
+
|
|
424
|
+
- `ResourceKey<T>` — symbol-typed key carrying the resource's value type as
|
|
425
|
+
a phantom type.
|
|
426
|
+
- `resource_key<T>(name)` — factory for module-scope resource keys.
|
|
427
|
+
- `world.has_resource(key)` — existence check.
|
|
428
|
+
- Resources are now plain key→value storage. `world.resource(key)` returns
|
|
429
|
+
the stored `T` directly.
|
|
430
|
+
|
|
431
|
+
#### Errors
|
|
432
|
+
|
|
433
|
+
- New `ECS_ERROR` categories: `RESOURCE_ALREADY_REGISTERED`,
|
|
434
|
+
`EVENT_ALREADY_REGISTERED`, `EVENT_NOT_REGISTERED`.
|
|
435
|
+
- New `TYPE_ERROR` category: `ASSERTION_FAIL_NON_NULLABLE`, emitted by the
|
|
436
|
+
new `assert_non_null` helper.
|
|
437
|
+
|
|
438
|
+
#### Assertions
|
|
439
|
+
|
|
440
|
+
- `assert_non_null<T>(value, message?)` in `type_primitives/assertions` —
|
|
441
|
+
dev-only (`__DEV__` guarded) assertion that narrows `T` to `NonNullable<T>`
|
|
442
|
+
and throws a `TypeError` with contextual info on failure.
|
|
443
|
+
|
|
444
|
+
#### New primitives
|
|
445
|
+
|
|
446
|
+
- `BinaryHeap<T>` in `type_primitives/binary_heap` — generic array-backed
|
|
447
|
+
heap with a user-supplied comparator. `push`, `pop`, `peek`, `clear`,
|
|
448
|
+
`size`. O(log n) push/pop, O(1) peek.
|
|
449
|
+
- `topological_sort<T>(nodes, edges, tiebreaker, node_name?)` in
|
|
450
|
+
`type_primitives/topological_sort` — Kahn's algorithm with a
|
|
451
|
+
`BinaryHeap`-backed ready queue for deterministic tie-breaking. Throws
|
|
452
|
+
`TypeError` on cycles; the schedule layer re-wraps as
|
|
453
|
+
`ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
|
|
454
|
+
|
|
455
|
+
#### Public exports
|
|
456
|
+
|
|
457
|
+
- `SystemFn`, `ReadonlyComponentRef`, `ChangedQuery`, `ReadonlyColumn`,
|
|
458
|
+
`ReadonlyUint32Array`, `EventKey`, `event_key`, `signal_key`,
|
|
459
|
+
`ResourceKey`, `resource_key` are now part of the package surface.
|
|
460
|
+
|
|
461
|
+
### Changed
|
|
462
|
+
|
|
463
|
+
- Query iteration is callback-based. `Query` no longer implements
|
|
464
|
+
`[Symbol.iterator]`. Iterate with `query.for_each((archetype) => { ... })`.
|
|
465
|
+
- `world.register_event`, `world.register_signal`, and `world.register_resource`
|
|
466
|
+
return `void` and take an `EventKey` / `ResourceKey` as their first argument.
|
|
467
|
+
- `world.emit`, `world.read`, `world.resource`, and `world.set_resource`
|
|
468
|
+
accept keys instead of definition objects. `world.resource(key)` returns
|
|
469
|
+
the typed value `T` directly rather than a field-reader wrapper.
|
|
470
|
+
- Schedule execution methods take a tick. `run_startup(label, tick)`,
|
|
471
|
+
`run_update(label, tick)`, and `run_fixed_update(label, tick)` require
|
|
472
|
+
the current frame tick. `ECS.update()` wires this automatically.
|
|
473
|
+
- System ordering now uses the shared `topological_sort` primitive. Observable
|
|
474
|
+
behavior is unchanged: `before`/`after` constraints respected,
|
|
475
|
+
`insertion_order` remains the tie-breaker, cycles surface as
|
|
476
|
+
`ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
|
|
477
|
+
- Store/query wiring. The store keeps a reference to each active `Query` via
|
|
478
|
+
`update_query_ref` and calls `mark_non_empty_dirty` only when structural
|
|
479
|
+
changes occur, avoiding spurious query rebuilds on stable frames.
|
|
480
|
+
- Bit-manipulation and hash constants (`BITS_PER_WORD`, `BITS_PER_WORD_SHIFT`,
|
|
481
|
+
`BITS_PER_WORD_MASK`, `FNV_OFFSET_BASIS`, `FNV_PRIME`) are exported from
|
|
482
|
+
`type_primitives/bitset` rather than `utils/constants`.
|
|
483
|
+
- Growable-array defaults (`DEFAULT_INITIAL_CAPACITY`, `GROWTH_FACTOR`) are
|
|
484
|
+
exported from `type_primitives/typed_arrays`.
|
|
485
|
+
|
|
486
|
+
### Fixed
|
|
487
|
+
|
|
488
|
+
- Query dirty propagation. `flush_destroyed` now marks affected queries dirty
|
|
489
|
+
so subsequent iteration sees the correct archetype set. `flush_structural`
|
|
490
|
+
skips dirty marking when no changes occurred.
|
|
491
|
+
- `set_field` on the world goes through `get_column_mut` with the current
|
|
492
|
+
tick, so mutations via the high-level API are visible to `ChangedQuery`.
|
|
493
|
+
|
|
494
|
+
### Removed
|
|
495
|
+
|
|
496
|
+
- `ResourceChannel`, `ResourceDef<F>`, `ResourceReader<F>`, `ResourceID`,
|
|
497
|
+
`as_resource_id`, and the `__resource_schema` marker symbol — the entire
|
|
498
|
+
SoA column-based resource storage layer. Resources are now key→value.
|
|
499
|
+
- `RESOURCE_ROW` constant — unused.
|
|
500
|
+
- `EventDef<F>` — replaced by `EventKey<F>`.
|
|
501
|
+
|
|
502
|
+
### Breaking changes
|
|
503
|
+
|
|
504
|
+
1. **Event definitions.** Define a key at module scope, then register and use it:
|
|
505
|
+
```ts
|
|
506
|
+
// before
|
|
507
|
+
const damage = world.register_event({ amount: "u32" } as const);
|
|
508
|
+
world.emit(damage, { amount: 5 });
|
|
509
|
+
|
|
510
|
+
// after
|
|
511
|
+
const DAMAGE = event_key<{ amount: "u32" }>("damage");
|
|
512
|
+
world.register_event(DAMAGE, { amount: "u32" } as const);
|
|
513
|
+
world.emit(DAMAGE, { amount: 5 });
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
2. **Resource registration / access.**
|
|
517
|
+
```ts
|
|
518
|
+
// before
|
|
519
|
+
const clock = world.register_resource({ ms: "u32" } as const, { ms: 0 });
|
|
520
|
+
const ms = world.resource(clock).ms;
|
|
521
|
+
|
|
522
|
+
// after
|
|
523
|
+
const CLOCK = resource_key<{ ms: number }>("clock");
|
|
524
|
+
world.register_resource(CLOCK, { ms: 0 });
|
|
525
|
+
const ms = world.resource(CLOCK).ms;
|
|
526
|
+
```
|
|
527
|
+
`world.resource()` returns the stored value directly; the reader wrapper
|
|
528
|
+
and the SoA column storage are gone.
|
|
529
|
+
|
|
530
|
+
3. **Query iteration.**
|
|
531
|
+
```ts
|
|
532
|
+
// before
|
|
533
|
+
for (const arch of query) { ... }
|
|
534
|
+
|
|
535
|
+
// after
|
|
536
|
+
query.for_each((arch) => { ... });
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
4. **Mutable vs readonly refs.** `query.ref(...)` now returns
|
|
540
|
+
`ReadonlyComponentRef`. Switch to `query.ref_mut(...)` when writing —
|
|
541
|
+
this is also what enables change detection for that component.
|
|
542
|
+
|
|
543
|
+
5. **Archetype column access.** `archetype.get_column(...)` returns a
|
|
544
|
+
`ReadonlyColumn`. Use `archetype.get_column_mut(def, field, tick)` for
|
|
545
|
+
direct writes. Most callers should use `query.ref_mut` and won't notice.
|
|
546
|
+
|
|
547
|
+
6. **Schedule driver signatures.** If you drive the scheduler directly
|
|
548
|
+
(bypassing `ECS.update()`), `run_startup`, `run_update`, and
|
|
549
|
+
`run_fixed_update` now require a `tick: number` argument.
|
|
550
|
+
|
|
551
|
+
## [0.2.1] and earlier
|
|
552
|
+
|
|
553
|
+
Prior releases — see git history.
|