@oasys/oecs 0.5.3 → 0.5.4
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 +194 -28
- package/README.md +198 -145
- package/dist/core/ecs/access_check.d.cts +3 -3
- package/dist/core/ecs/access_check.d.ts +3 -3
- package/dist/core/ecs/archetype.d.cts +103 -43
- package/dist/core/ecs/archetype.d.ts +103 -43
- package/dist/core/ecs/archetype.d.ts.map +1 -1
- package/dist/core/ecs/archetype_graph.d.cts +8 -8
- package/dist/core/ecs/archetype_graph.d.ts +8 -8
- package/dist/core/ecs/archetype_graph.d.ts.map +1 -1
- package/dist/core/ecs/builtin_relations.d.cts +1 -1
- package/dist/core/ecs/builtin_relations.d.ts +1 -1
- package/dist/core/ecs/command_log.d.cts +4 -4
- package/dist/core/ecs/command_log.d.ts +4 -4
- package/dist/core/ecs/command_log.d.ts.map +1 -1
- package/dist/core/ecs/component.d.cts +4 -4
- package/dist/core/ecs/component.d.ts +4 -4
- package/dist/core/ecs/deferred_commands.d.cts +1 -1
- package/dist/core/ecs/deferred_commands.d.ts +1 -1
- package/dist/core/ecs/deferred_commands.d.ts.map +1 -1
- package/dist/core/ecs/ecs.d.cts +85 -43
- package/dist/core/ecs/ecs.d.ts +85 -43
- package/dist/core/ecs/ecs.d.ts.map +1 -1
- package/dist/core/ecs/ecs_memory.d.cts +10 -10
- package/dist/core/ecs/ecs_memory.d.ts +10 -10
- package/dist/core/ecs/ecs_memory.d.ts.map +1 -1
- package/dist/core/ecs/entity_allocator.d.cts +6 -6
- package/dist/core/ecs/entity_allocator.d.ts +6 -6
- package/dist/core/ecs/entity_allocator.d.ts.map +1 -1
- package/dist/core/ecs/event.d.cts +2 -2
- package/dist/core/ecs/event.d.ts +2 -2
- package/dist/core/ecs/event_registry.d.cts +1 -1
- package/dist/core/ecs/event_registry.d.ts +1 -1
- package/dist/core/ecs/event_registry.d.ts.map +1 -1
- package/dist/core/ecs/facades.d.cts +15 -15
- package/dist/core/ecs/facades.d.ts +15 -15
- package/dist/core/ecs/facades.d.ts.map +1 -1
- package/dist/core/ecs/frame_trace.d.cts +2 -2
- package/dist/core/ecs/frame_trace.d.ts +2 -2
- package/dist/core/ecs/host_commands.d.cts +12 -12
- package/dist/core/ecs/host_commands.d.ts +12 -12
- package/dist/core/ecs/host_commands.d.ts.map +1 -1
- package/dist/core/ecs/index.d.cts +1 -1
- package/dist/core/ecs/index.d.ts +1 -1
- package/dist/core/ecs/index.d.ts.map +1 -1
- package/dist/core/ecs/observer.d.cts +9 -9
- package/dist/core/ecs/observer.d.ts +9 -9
- package/dist/core/ecs/observer.d.ts.map +1 -1
- package/dist/core/ecs/query.d.cts +91 -70
- package/dist/core/ecs/query.d.ts +91 -70
- package/dist/core/ecs/query.d.ts.map +1 -1
- package/dist/core/ecs/ref.d.cts +53 -1
- package/dist/core/ecs/ref.d.ts +53 -1
- package/dist/core/ecs/ref.d.ts.map +1 -1
- package/dist/core/ecs/relation.d.cts +15 -15
- package/dist/core/ecs/relation.d.ts +15 -15
- package/dist/core/ecs/relation.d.ts.map +1 -1
- package/dist/core/ecs/relation_service.d.cts +14 -14
- package/dist/core/ecs/relation_service.d.ts +14 -14
- package/dist/core/ecs/relation_service.d.ts.map +1 -1
- package/dist/core/ecs/resource.d.cts +1 -1
- package/dist/core/ecs/resource.d.ts +1 -1
- package/dist/core/ecs/resource_registry.d.cts +2 -2
- package/dist/core/ecs/resource_registry.d.ts +2 -2
- package/dist/core/ecs/resume.d.cts +3 -3
- package/dist/core/ecs/resume.d.ts +3 -3
- package/dist/core/ecs/resume.d.ts.map +1 -1
- package/dist/core/ecs/schedule.d.cts +22 -9
- package/dist/core/ecs/schedule.d.ts +22 -9
- package/dist/core/ecs/schedule.d.ts.map +1 -1
- package/dist/core/ecs/snapshot_service.d.cts +3 -3
- package/dist/core/ecs/snapshot_service.d.ts +3 -3
- package/dist/core/ecs/sparse_store.d.cts +6 -6
- package/dist/core/ecs/sparse_store.d.ts +6 -6
- package/dist/core/ecs/sparse_store.d.ts.map +1 -1
- package/dist/core/ecs/store.d.cts +189 -110
- package/dist/core/ecs/store.d.ts +189 -110
- package/dist/core/ecs/store.d.ts.map +1 -1
- package/dist/core/ecs/store_layout_listener.d.cts +2 -2
- package/dist/core/ecs/store_layout_listener.d.ts +2 -2
- package/dist/core/ecs/system.d.cts +8 -8
- package/dist/core/ecs/system.d.ts +8 -8
- package/dist/core/ecs/system.d.ts.map +1 -1
- package/dist/core/ecs/utils/error.d.cts +7 -0
- package/dist/core/ecs/utils/error.d.ts +7 -0
- package/dist/core/ecs/utils/error.d.ts.map +1 -1
- package/dist/core/store/action_ring.d.cts +7 -8
- package/dist/core/store/action_ring.d.ts +7 -8
- package/dist/core/store/action_ring.d.ts.map +1 -1
- package/dist/core/store/allocator.d.cts +26 -28
- package/dist/core/store/allocator.d.ts +26 -28
- package/dist/core/store/allocator.d.ts.map +1 -1
- package/dist/core/store/buffer_backed_column.d.cts +5 -6
- package/dist/core/store/buffer_backed_column.d.ts +5 -6
- package/dist/core/store/buffer_backed_column.d.ts.map +1 -1
- package/dist/core/store/column_store.d.cts +22 -22
- package/dist/core/store/column_store.d.ts +22 -22
- package/dist/core/store/column_store.d.ts.map +1 -1
- package/dist/core/store/command_dispatch.d.cts +1 -1
- package/dist/core/store/command_dispatch.d.ts +1 -1
- package/dist/core/store/command_ring.d.cts +6 -7
- package/dist/core/store/command_ring.d.ts +6 -7
- package/dist/core/store/command_ring.d.ts.map +1 -1
- package/dist/core/store/descriptor.d.cts +2 -2
- package/dist/core/store/descriptor.d.ts +2 -2
- package/dist/core/store/entity_index.d.cts +3 -3
- package/dist/core/store/entity_index.d.ts +3 -3
- package/dist/core/store/event_ring.d.cts +7 -7
- package/dist/core/store/event_ring.d.ts +7 -7
- package/dist/core/store/extend.d.cts +2 -2
- package/dist/core/store/extend.d.ts +2 -2
- package/dist/core/store/grow.d.cts +1 -1
- package/dist/core/store/grow.d.ts +1 -1
- package/dist/core/store/grow.d.ts.map +1 -1
- package/dist/core/store/header.d.cts +9 -9
- package/dist/core/store/header.d.ts +9 -9
- package/dist/core/store/header.d.ts.map +1 -1
- package/dist/core/store/layout_ops.d.cts +7 -7
- package/dist/core/store/layout_ops.d.ts +7 -7
- package/dist/core/store/state_hash.d.cts +3 -3
- package/dist/core/store/state_hash.d.ts +3 -3
- package/dist/extensions/editor/editor.d.cts +4 -4
- package/dist/extensions/editor/editor.d.ts +4 -4
- package/dist/extensions/editor/editor.d.ts.map +1 -1
- package/dist/extensions/editor/field_handle.d.cts +1 -1
- package/dist/extensions/editor/field_handle.d.ts +1 -1
- package/dist/extensions/editor/index.d.cts +2 -2
- package/dist/extensions/editor/index.d.ts +2 -2
- package/dist/extensions/editor/index.development.js +4 -4
- package/dist/extensions/editor/index.js +4 -4
- package/dist/extensions/reactive/ecs_sync.d.cts +4 -4
- package/dist/extensions/reactive/ecs_sync.d.ts +4 -4
- package/dist/extensions/reactive/index.d.cts +4 -4
- package/dist/extensions/reactive/index.d.ts +4 -4
- package/dist/extensions/reactive/index.development.js +3 -3
- package/dist/extensions/reactive/index.js +3 -3
- package/dist/extensions/solid/index.d.cts +1 -1
- package/dist/extensions/solid/index.d.ts +1 -1
- package/dist/extensions/solid/index.development.js +1 -1
- package/dist/extensions/solid/index.js +1 -1
- package/dist/extensions/solid/kernel_solid.d.cts +1 -1
- package/dist/extensions/solid/kernel_solid.d.ts +1 -1
- package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
- package/dist/{host_commands-3jyFfWNg.js → host_commands-BlR3oZ3x.js} +472 -392
- package/dist/host_commands-C2cpU3yC.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.development.cjs +1 -1
- package/dist/index.development.js +3344 -2965
- package/dist/index.js +1494 -1132
- package/dist/{internal-C9jjL90H.js → internal-BoxWl4dG.js} +409 -329
- package/dist/internal-CGBEwfeB.cjs +2 -0
- package/dist/internal.cjs +1 -1
- package/dist/internal.development.cjs +1 -1
- package/dist/internal.development.js +1 -1
- package/dist/internal.js +1 -1
- package/dist/primitives.development.js +1 -1
- package/dist/primitives.js +1 -1
- package/dist/reactive/index.d.cts +2 -2
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.development.js +1 -1
- package/dist/reactive/index.js +1 -1
- package/dist/reactive/kernel.d.cts +5 -5
- package/dist/reactive/kernel.d.ts +5 -5
- package/dist/reactive/shallow.d.cts +1 -1
- package/dist/reactive/shallow.d.ts +1 -1
- package/dist/{shallow-DfnAm9vG.js → shallow-Bs39EPh1.js} +1 -1
- package/dist/{shallow-9S1pY_Iw.js → shallow-CiFC70Dz.js} +1 -1
- package/dist/{shared-DQKK0i-E.js → shared-B4v0L1cw.js} +4 -4
- package/dist/shared-DU78k0pU.cjs +1 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.development.cjs +1 -1
- package/dist/shared.development.js +1 -1
- package/dist/shared.js +1 -1
- package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +2 -2
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +2 -2
- package/dist/{typed_arrays-BcXcnzB4.js → typed_arrays-DIVwVy5_.js} +1 -1
- package/dist/version.d.cts +2 -2
- package/dist/version.d.ts +2 -2
- package/package.json +1 -1
- package/dist/host_commands-DNf0f6ko.cjs +0 -1
- package/dist/internal-RZN14uMw.cjs +0 -2
- package/dist/shared-BMXh9hxm.cjs +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,28 +5,193 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.4] — 2026-07-31
|
|
9
|
+
|
|
10
|
+
### Added — `cursor` and `cursorRead`, the accessor for a sweep by id
|
|
11
|
+
|
|
12
|
+
`ecs.cursor(def)` and `ecs.cursorRead(def)`, with the two equivalents on `ctx`, give a single-entity
|
|
13
|
+
accessor that you can **point again**. You create it one time, and then you aim it with
|
|
14
|
+
`at(entity)`.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const p = ctx.cursor(Pos);
|
|
18
|
+
for (let i = 0; i < ids.length; i++) {
|
|
19
|
+
p.at(ids[i]);
|
|
20
|
+
p.x += p.y * dt;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The engine creates a ref for each entity. Over a list of ids, the loop then discards each ref that
|
|
25
|
+
it created. A cursor lifts that creation out of the loop: `at` writes the archetype, the offset, and
|
|
26
|
+
the row, and nothing else. A cursor also resolves the position of each field when you create it, so
|
|
27
|
+
a read does not look up a field name.
|
|
28
|
+
|
|
29
|
+
A cursor is **safer than a ref that you hold**, and not more dangerous. `at` resolves the archetype
|
|
30
|
+
and the row again on each call. So a structural change between two `at` calls cannot make the cursor
|
|
31
|
+
read a different entity. A cursor also follows an entity that changes archetype, which a ref cannot
|
|
32
|
+
do. Only the window between one `at` call and the reads that follow it must be free of structural
|
|
33
|
+
change.
|
|
34
|
+
|
|
35
|
+
In a development build, a cursor makes its declared-access check on **each `at`**, and not only when
|
|
36
|
+
you create it. `cursor` makes a write check, and `cursorRead` makes a read check. The check is on
|
|
37
|
+
`at` because you keep a cursor, and a cursor can therefore outlive the system that made it. A cursor
|
|
38
|
+
that you make outside a system, or in a different system, is checked against the system that
|
|
39
|
+
**uses** it. An `at` outside every system makes no check, because no system can hold the fault. A
|
|
40
|
+
production build removes this check, as it removes each of the other development guards.
|
|
41
|
+
|
|
42
|
+
A cursor obeys the rules of the family: the definition comes first, the mutable name has no suffix,
|
|
43
|
+
and the read-only name has the `Read` suffix. There is one constraint. A component with a field
|
|
44
|
+
named `at` collides with the method of the cursor. Creation of the cursor rejects that component,
|
|
45
|
+
and the message says so.
|
|
46
|
+
|
|
47
|
+
See [refs and cursors](docs/api/refs.md#cursors--many-entities-by-id).
|
|
48
|
+
|
|
49
|
+
### Added — `ECS_ERROR.ARCHETYPE_ROW_INVARIANT`
|
|
50
|
+
|
|
51
|
+
This error reports that the row bookkeeping of an archetype does not agree with its backing columns.
|
|
52
|
+
There are three causes. A reserve did not give the capacity that the engine asked for. A restore
|
|
53
|
+
gave a partition boundary that is out of range. Or a cached row plane points at a buffer that is no
|
|
54
|
+
longer current. The error is for development builds only. It reports a failure of an internal
|
|
55
|
+
invariant, and not a mistake by the caller. That is what makes it different from
|
|
56
|
+
`STORE_CAP_EXCEEDED`, which is the allocator that refuses a legitimate grow. Two development
|
|
57
|
+
assertions that reported the general `COMPONENT_NOT_REGISTERED` now use it.
|
|
58
|
+
|
|
59
|
+
### Changed — structural churn, system dispatch, and fragmented iteration
|
|
60
|
+
|
|
61
|
+
No signature changed on a function that exists, and no result changed. Each entry below is a change
|
|
62
|
+
to the internal mechanism. What moved:
|
|
63
|
+
|
|
64
|
+
- **The row plane of an archetype.** Row placement went through the `ColumnBacking` API (`push`,
|
|
65
|
+
`swapRemove`, and `pop`). That API costs three things **for each column and for each row**: a call
|
|
66
|
+
to the `.buf` accessor, a comparison against the capacity, and a load and store of `_len`. The
|
|
67
|
+
actual work is one move of an element. But `Archetype.length` is already the row count of every
|
|
68
|
+
column. So the archetype now indexes cached raw views (`_bufs[i][row]`), and it moves `length` one
|
|
69
|
+
time. The probe for an overflow on an append becomes one comparison against a cached capacity.
|
|
70
|
+
- **`eachChunk` no longer refreshes a column group on each call.** `cols.mut(def)` and
|
|
71
|
+
`cols.read(def)` used one cached object for each (archetype, component) pair. But they wrote one
|
|
72
|
+
property for each field on *every* call, and a fragmented pass makes that call one time for each
|
|
73
|
+
chunk. Only `_syncRowPlane` can change the identity of the buffer of a column. So `_syncRowPlane`
|
|
74
|
+
now points the cached groups at the current buffers, and the accessors make no test for a stale
|
|
75
|
+
buffer.
|
|
76
|
+
- **`readField` indexes the row plane.** It reads `_bufs[i][row]`, and not
|
|
77
|
+
`_flatColumns[i].buf[row]`. This removes a `.buf` accessor whose concrete type is different for a
|
|
78
|
+
heap column and for a `SharedArrayBuffer` column.
|
|
79
|
+
- **The last-run ticks of the schedule.** `systemLastRun` was a `Map<SystemDescriptor, number>`, and
|
|
80
|
+
a phase read it and wrote it one time for each system. It is now a packed array that a slot, local
|
|
81
|
+
to the schedule, indexes. The slots travel in the cached phase plan, next to the sorted
|
|
82
|
+
descriptors. `hasFixedSystems()` holds the node list of `FIXED_UPDATE` directly, and it makes no
|
|
83
|
+
lookup by key for each frame.
|
|
84
|
+
- **One probe of the edge for a single add or remove.** `addComponent` made four lookups before it
|
|
85
|
+
touched a row: `mask.has`, `archResolveAdd`, which read both again, `archGet`, and then a second
|
|
86
|
+
`getEdge` for the transition map. An `edge.add` value that is not null means exactly "this
|
|
87
|
+
archetype does not hold the component, and the destination is resolved". So one probe of the holey
|
|
88
|
+
`edges` array answers all of it. The first-sight case and the overwrite-in-place case move to a
|
|
89
|
+
cold tail. `removeComponent` is the mirror image.
|
|
90
|
+
- **One resolve of liveness and index, in place of two derivations.** `Store.hasComponent` called
|
|
91
|
+
`isAlive`, which derived the packed entity index. It then derived that index a second time to
|
|
92
|
+
reach `entityArchetype`. The read of the generations also went through a call into the allocator.
|
|
93
|
+
Both now fold into one `_liveIndex`. `getEntityArchetype` and `getEntityRow` become one
|
|
94
|
+
`resolveEntity`.
|
|
95
|
+
- **`ecs.query(...)` allocates nothing when the cache holds the query.** The caller copied its
|
|
96
|
+
scratch mask before it gave that mask to the resolver, and the resolver copies each mask that it
|
|
97
|
+
keeps. That copy made a `BitSet` and a backing `number[]` for each call, for nothing. The contract
|
|
98
|
+
for the borrowed mask is now written at the resolver.
|
|
99
|
+
- **`clearEvents` returns immediately when the frame emitted no event.** A write of `length` on an
|
|
100
|
+
array is a property store, and V8 does not remove that store for an array that is already empty.
|
|
101
|
+
This ran one time for each `update()` call, and most phases emit no event.
|
|
102
|
+
|
|
103
|
+
### Changed — diagnostic vocabulary catch-up (the deferred snake_case remnants)
|
|
104
|
+
|
|
105
|
+
- **BREAKING (diagnostics):** `ECS.memoryPlan.source` now reports `"maxBytes"` instead of
|
|
106
|
+
`"max_bytes"` for the arm with the explicit byte cap. The name now agrees with the option key
|
|
107
|
+
that it names. Every other arm already agreed: `budget`, `heap`, `shared`, `wasm`, `allocator`,
|
|
108
|
+
and `default`.
|
|
109
|
+
- The `INVALID_MEMORY_OPTIONS` messages and the `memoryPlan.derivation` trace now name the options
|
|
110
|
+
by their real camelCase keys (`columnCapacity`, `entityIndex`, `budget.bytesPerEntity`,
|
|
111
|
+
`wasm.maximumPages`, `wasm.initialPages`, and `capBytesHint`), and not by the snake_case
|
|
112
|
+
spellings from before 0.4.
|
|
113
|
+
|
|
114
|
+
### Fixed — row-plane and schedule-slot correctness under the new caches
|
|
115
|
+
|
|
116
|
+
- **A grow that throws no longer leaves the row plane on a buffer that the engine released.** The
|
|
117
|
+
reserve grows the entity-id array on the heap before it asks the store to grow the columns. So
|
|
118
|
+
when a refusal of the `SharedArrayBuffer` cap threw out of the grow handler, the cached entity-id
|
|
119
|
+
view pointed at a buffer that the engine had released, and the array itself had moved. The world
|
|
120
|
+
must survive a refusal of the cap, because that refusal is the basis of the fail-closed `spawn`
|
|
121
|
+
and `spawnMany` contract. But a later append that fitted the stale capacity wrote its entity id
|
|
122
|
+
into the released buffer. The next re-sync then put in the buffer that never received that row. A
|
|
123
|
+
re-sync occurs on a successful grow, and on the `refreshViews` call that each new archetype
|
|
124
|
+
causes. The id then read back as `0`, and the swap-remove that followed corrupted the row pointer
|
|
125
|
+
of a *different live entity*. The engine now derives the plane again on the path that throws.
|
|
126
|
+
- **A shortfall of capacity in the entity-id array alone no longer causes a reallocation of the
|
|
127
|
+
full store.** The cached row capacity is the smaller of two values: the capacity of the entity-id
|
|
128
|
+
array, and the capacity of every column. But the *decision to grow* belongs to the column term
|
|
129
|
+
alone. A decision on the smaller value sent a shortfall to the store when only the entity-id array
|
|
130
|
+
had one, and that array had already grown. The store then calculated a capacity that did not
|
|
131
|
+
change, and it found no column to resize. But it still did a full snapshot, create, and restore of
|
|
132
|
+
the full column store, plus a `refreshViews` call on each archetype. It did all of that to resize
|
|
133
|
+
nothing. This is reachable after a restore from a snapshot, because a restore grows the entity-id
|
|
134
|
+
array to the *number* of restored rows, and not to its capacity.
|
|
135
|
+
- **A recycled `systemLastRun` slot can no longer be the slot of two systems.** A phase copies the
|
|
136
|
+
slot array of its plan into a local. So a system that you remove from inside that phase still runs
|
|
137
|
+
from the snapshot, and it still writes its last-run tick as it ends. Before this fix, the engine
|
|
138
|
+
could give that freed slot to a system that you added during the same drive. The write of the
|
|
139
|
+
removed system then landed on the tick of the new system. That write moved the `changed()` window
|
|
140
|
+
of the new system, and it gave no signal. The engine now recycles a slot only outside a running
|
|
141
|
+
drive. Between drives the slots recycle as before, so the array stays bounded.
|
|
142
|
+
|
|
143
|
+
### Note — the lookup of a field name stays as it is
|
|
144
|
+
|
|
145
|
+
`getField` resolves a field name through the `_fieldIndex[cid][field]` table. The engine builds that
|
|
146
|
+
table with `Object.create(null)`, which puts it in dictionary mode. The investigation covered three
|
|
147
|
+
replacements: a `{}` literal, a `Map`, and interning of the names with a perfect hash for each
|
|
148
|
+
component. None of the three is better than the table that exists in a world that has many
|
|
149
|
+
components with different field names. The table stays, and a comment next to it records the
|
|
150
|
+
investigation. To remove the lookup, the caller must hold the ordinal of the field, and not its name.
|
|
151
|
+
`cursor` does exactly that for a sweep. To do the measurement again, use
|
|
152
|
+
`bench/vs/probe-fieldshape.mjs` and `bench/vs/probe-lookupcost.mjs`.
|
|
153
|
+
|
|
154
|
+
### Docs
|
|
155
|
+
|
|
156
|
+
- `ARCHITECTURE.md` carries the 0.5.4 stamp. It now describes the row plane of an archetype, the
|
|
157
|
+
packed last-run slots of the schedule, and the `ARCHETYPE_ROW_INVARIANT` assertion.
|
|
158
|
+
- `ARCHITECTURE.md` gives **no line numbers**. A reference names its source file only, because a
|
|
159
|
+
line number becomes incorrect as the source changes. To find a claim, search for the name of the
|
|
160
|
+
symbol next to the reference.
|
|
161
|
+
- The documentation gives no benchmark figures. Each entry describes what changed.
|
|
162
|
+
- `ARCHITECTURE.md`, `README.md`, `api/memory.md`, `api/index.md`, and `BEST_PRACTICES.md` no
|
|
163
|
+
longer describe the default heap backing as a growable or resizable `ArrayBuffer`. It is fixed at
|
|
164
|
+
the cap, and has been since 0.5.3.
|
|
165
|
+
- `api/refs.md` documents cursors, and `api/errors.md` documents
|
|
166
|
+
`ARCHETYPE_ROW_INVARIANT`. The error count in `api/errors.md` is now 48.
|
|
167
|
+
|
|
168
|
+
### Packaging
|
|
169
|
+
|
|
170
|
+
- `bench/` is no longer part of the JSR package. npm ships from a list of the files to include
|
|
171
|
+
(`files: ["dist", "CHANGELOG.md"]`), so npm was never affected. But JSR publishes the source tree
|
|
172
|
+
against a list of the files to exclude, and the local bench and oracle harnesses would have
|
|
173
|
+
shipped as soon as git tracked them.
|
|
174
|
+
|
|
8
175
|
## [0.5.3] — 2026-07-09
|
|
9
176
|
|
|
10
|
-
### Fixed — heap columns
|
|
177
|
+
### Fixed — heap columns are again on the fast element-access path of V8
|
|
11
178
|
|
|
12
179
|
- The pure-TS **heap profile** (`heapArraybufferAllocator` — the default backing)
|
|
13
180
|
now reserves its store as a **fixed, non-resizable `ArrayBuffer`** at the full
|
|
14
|
-
cap
|
|
15
|
-
element-access path for
|
|
16
|
-
`ArrayBuffer
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
cross-library bench
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
reservation (a 256 MiB default cap on a 1000-entity world stays a few MiB
|
|
25
|
-
resident — measured ~4 MiB RSS, within ~1 MiB of the old resizable buffer).
|
|
181
|
+
cap. Before this release it used a growable buffer, and it made that buffer
|
|
182
|
+
larger with `.resize()`. V8 has no fast element-access path for a TypedArray
|
|
183
|
+
view over a **resizable or growable** `ArrayBuffer`, because each `col[i]`
|
|
184
|
+
reads the mutable length again. Thus a loop over a column was much slower than
|
|
185
|
+
the same loop over a fixed buffer. This made each iteration-bound system slower
|
|
186
|
+
from 0.3.x, and it gave no signal. Version 0.5.3 corrects the fault, and a
|
|
187
|
+
cross-library bench gives the throughput of 0.3.1 again.
|
|
188
|
+
- The fixed buffer faults pages in lazily. Thus RSS follows the real use, and not
|
|
189
|
+
the reservation: a world with few entities keeps a small resident set at the
|
|
190
|
+
256 MiB default cap, which is equivalent to the old resizable buffer.
|
|
26
191
|
Growth remains in place: the store relocates columns within the pre-reserved
|
|
27
192
|
buffer, so the buffer identity never changes and every existing view stays
|
|
28
193
|
valid —
|
|
29
|
-
`isInPlace: true` and
|
|
194
|
+
`isInPlace: true` and the entity-index-hoist-across-grow invariant hold
|
|
30
195
|
unchanged. The store keys its tail cursor off the header `capacity` (the
|
|
31
196
|
logical high-water) rather than `buffer.byteLength` (now always the cap).
|
|
32
197
|
- Only the heap backing changed. The `growable_sab` / `wasm_memory` backings
|
|
@@ -165,7 +330,8 @@ rename/removal map:
|
|
|
165
330
|
| `WorldRestoreError` / `WORLD_SNAPSHOT_VERSION` | `ECSRestoreError` / `ECS_SNAPSHOT_VERSION` |
|
|
166
331
|
|
|
167
332
|
- **Host `despawn` is immediate** — `ecs.despawn(e); ecs.isAlive(e)` is `false` on the next
|
|
168
|
-
line
|
|
333
|
+
line. This removes the inconsistency: host `addComponent` was immediate, but destroy was
|
|
334
|
+
buffered.
|
|
169
335
|
**Observer note:** like every immediate op, host `despawn` fires no *structural* observers —
|
|
170
336
|
`onRemove` no longer sees host-despawned entities (it did at 0.4, when host destroy was
|
|
171
337
|
deferred). Observer-driven consumers, including the `reactive-sync` map bridges, only see
|
|
@@ -177,7 +343,7 @@ rename/removal map:
|
|
|
177
343
|
`ctx.commands` equivalent. Mid-system these ops can move rows a running query is walking and
|
|
178
344
|
are invisible to observers; previously only `despawn` was guarded wholesale (the others were
|
|
179
345
|
caught only when they touched the archetype being iterated). Cross-world host mutation from
|
|
180
|
-
another world's system
|
|
346
|
+
another world's system is unaffected — the guard is scoped to the mutated world.
|
|
181
347
|
- **The bare deferred duplicates on `ctx` are removed** — `ctx.addComponent`,
|
|
182
348
|
`ctx.removeComponent`, `ctx.disable`, `ctx.enable` join the already-removed
|
|
183
349
|
`ctx.createEntity` / `ctx.destroyEntity`. `ctx.commands` is now the *only* deferred surface,
|
|
@@ -186,7 +352,7 @@ rename/removal map:
|
|
|
186
352
|
`ctx.addComponent` carried, so compile-checked complete attaches survive the move.
|
|
187
353
|
`ctx.isDisabled` stays (immediate read), as do the immediate sparse/relation ops.
|
|
188
354
|
- **`sourcesOf` canonicalized to `(entity, def)`** on `ecs.relations` and `SystemContext` —
|
|
189
|
-
it was the one arg-order outlier on the relation surface
|
|
355
|
+
it was the one arg-order outlier on the relation surface.
|
|
190
356
|
- **The package root is now a curated, explicit export list** — `export *` no longer flattens the
|
|
191
357
|
whole core barrel, so future barrel additions cannot silently widen the public API. A checked-in
|
|
192
358
|
public-API snapshot test makes any surface change an explicit diff in review.
|
|
@@ -200,14 +366,14 @@ rename/removal map:
|
|
|
200
366
|
### Added
|
|
201
367
|
|
|
202
368
|
- **`addComponent` bundle overload** — `ecs.addComponent(e, Pos({ x: 1 }))` accepts a bundle
|
|
203
|
-
with the usual zero-fill semantics
|
|
369
|
+
with the usual zero-fill semantics; the explicit `(e, def, values)` form stays
|
|
204
370
|
complete-values, so a typo'd or missing field is still a compile error.
|
|
205
371
|
- **`spawnMany` typed template + shared overrides** — bulk spawn takes the same typed
|
|
206
372
|
`Template<Defs>` as `spawn` plus one optional `TemplateOverrides<Defs>` object applied to
|
|
207
373
|
every row (contiguous batches use one `fill` per overridden column).
|
|
208
374
|
- **JSDoc `@example` on the core surface** — `registerComponent`, `spawn`, `addComponent`,
|
|
209
375
|
`query`, `registerSystem`, `startup`, `update`, `ctx.emit` / `ctx.read`,
|
|
210
|
-
`events.register`, `resources.register` now carry hover-visible examples
|
|
376
|
+
`events.register`, `resources.register` now carry hover-visible examples.
|
|
211
377
|
- **Component debug names** — `registerComponent(schema, { name: "Pos" })` (and the sparse
|
|
212
378
|
sibling) records a diagnostic label, so access-violation and liveness errors read
|
|
213
379
|
`'Pos' (component 5)` instead of leaving you to count registration order
|
|
@@ -258,13 +424,13 @@ rename/removal map:
|
|
|
258
424
|
ops on an archetype a live dense walk is standing in now throw in dev, *before* any mutation
|
|
259
425
|
lands (the transition path checks ahead of the destination append, so no dual-residency
|
|
260
426
|
half-state). Collect ids during the walk and mutate after it. Mutating archetypes the walk is
|
|
261
|
-
*not* currently visiting stays legal — the
|
|
427
|
+
*not* currently visiting stays legal — the fresh-snapshot machinery still covers those.
|
|
262
428
|
- **Cross-world despawn false positive** — `worldB.despawn(e)` from inside world A's system no
|
|
263
429
|
longer trips the in-system despawn guard (the accessCheck span is process-global; the guard now
|
|
264
|
-
also requires *this* world to be mid-schedule). Driving a second world from a system
|
|
430
|
+
also requires *this* world to be mid-schedule). Driving a second world from a system
|
|
265
431
|
mutates it host-style, which is safe — B is not iterating. Unnamed systems in the guard message
|
|
266
432
|
now render as `system_<id>` instead of `'?'`.
|
|
267
|
-
- **Frame trace records every deferred command
|
|
433
|
+
- **Frame trace records every deferred command** — the removed bare `ctx.*` deferred
|
|
268
434
|
forms bypassed the `commandQueued` trace hook, so host-command-seam adds/removes/toggles (and
|
|
269
435
|
any system using the bare forms) were invisible to an attached `FrameTraceSink` while their
|
|
270
436
|
spawns/despawns were visible. With `ctx.commands` as the only deferred surface every queued
|
|
@@ -272,7 +438,7 @@ rename/removal map:
|
|
|
272
438
|
(previously only the spawn itself).
|
|
273
439
|
- **Stale deferred-attach docs** — `host_commands.ts` / the host-write-seam page claimed the
|
|
274
440
|
deferred add path does not zero-fill omitted fields (NaN readback); every attach path
|
|
275
|
-
zero-fills
|
|
441
|
+
zero-fills (`writeFields`'s `?? 0`). The complete-values requirement on
|
|
276
442
|
`SpawnEntry` is documented as what it is — explicit intent in a reified, replayable record —
|
|
277
443
|
and the observer docs now scope "immediate ops fire no observers" to *structural* observers
|
|
278
444
|
(`onSet` is derived change detection and sees host `setField` writes).
|
|
@@ -309,11 +475,11 @@ rename/removal map:
|
|
|
309
475
|
`runEveryNTicks` validation throws `ECSError` (`INVALID_RUN_CONDITION`).
|
|
310
476
|
- **Docs standardized on the `ecs` receiver** — README, GETTING_STARTED, BEST_PRACTICES, the
|
|
311
477
|
api reference, and every in-source JSDoc example now spell `const ecs = new ECS()`
|
|
312
|
-
(
|
|
478
|
+
(with the `World*` names renamed to `ECS*`, "world" survives only as prose). The
|
|
313
479
|
host-write-seam docs now explain *why* `queue.spawn` takes complete-value `spawnEntry`s
|
|
314
|
-
rather than zero-filling bundles
|
|
480
|
+
rather than zero-filling bundles: commands are a reified, replayable record — complete
|
|
315
481
|
values are explicit intent legible to replay, not a correctness need; the deferred add path
|
|
316
|
-
zero-fills omitted fields
|
|
482
|
+
zero-fills omitted fields.
|
|
317
483
|
- **JSR publish no longer ships `__tests__` helper files** (`casing_codemod.ts`,
|
|
318
484
|
`test_helpers.ts` — including a `node:fs` import subject to JSR type-checking).
|
|
319
485
|
|
|
@@ -506,8 +672,8 @@ Performance-only patch release. Two targeted allocation-elimination changes on h
|
|
|
506
672
|
|
|
507
673
|
### Performance
|
|
508
674
|
|
|
509
|
-
- **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:
|
|
510
|
-
- **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
|
|
675
|
+
- **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 on the same workload: a higher throughput of `add_components` on an already-populated entity, a much smaller peak heap, and a much smaller peak RSS. ([#9](https://github.com/oasys-works/oecs/pull/9))
|
|
676
|
+
- **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 on a compose loop with four shapes: a much higher throughput, a much smaller peak heap, and almost no growth of RSS during the workload. ([#10](https://github.com/oasys-works/oecs/pull/10))
|
|
511
677
|
|
|
512
678
|
## [0.3.0] — 2026-04-21
|
|
513
679
|
|