@oasys/oecs 0.5.4 → 0.6.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 +1223 -266
- package/README.md +217 -112
- package/dist/core/ecs/access_check.d.cts +29 -29
- package/dist/core/ecs/access_check.d.ts +29 -29
- package/dist/core/ecs/access_check.d.ts.map +1 -1
- package/dist/core/ecs/archetype.d.cts +217 -209
- package/dist/core/ecs/archetype.d.ts +217 -209
- package/dist/core/ecs/archetype.d.ts.map +1 -1
- package/dist/core/ecs/archetype_closure.d.cts +23 -0
- package/dist/core/ecs/archetype_closure.d.ts +23 -0
- package/dist/core/ecs/archetype_closure.d.ts.map +1 -0
- package/dist/core/ecs/archetype_graph.d.cts +33 -33
- package/dist/core/ecs/archetype_graph.d.ts +33 -33
- package/dist/core/ecs/archetype_graph.d.ts.map +1 -1
- package/dist/core/ecs/archetype_types.d.cts +55 -0
- package/dist/core/ecs/archetype_types.d.ts +55 -0
- package/dist/core/ecs/archetype_types.d.ts.map +1 -0
- package/dist/core/ecs/change_feed.d.cts +53 -0
- package/dist/core/ecs/change_feed.d.ts +53 -0
- package/dist/core/ecs/change_feed.d.ts.map +1 -0
- package/dist/core/ecs/changed_query.d.cts +36 -0
- package/dist/core/ecs/changed_query.d.ts +36 -0
- package/dist/core/ecs/changed_query.d.ts.map +1 -0
- package/dist/core/ecs/chunk_columns.d.cts +49 -0
- package/dist/core/ecs/chunk_columns.d.ts +49 -0
- package/dist/core/ecs/chunk_columns.d.ts.map +1 -0
- package/dist/core/ecs/command_log.d.cts +26 -26
- package/dist/core/ecs/command_log.d.ts +26 -26
- package/dist/core/ecs/command_log.d.ts.map +1 -1
- package/dist/core/ecs/component.d.cts +43 -42
- package/dist/core/ecs/component.d.ts +43 -42
- package/dist/core/ecs/component.d.ts.map +1 -1
- package/dist/core/ecs/component_registry.d.cts +53 -0
- package/dist/core/ecs/component_registry.d.ts +53 -0
- package/dist/core/ecs/component_registry.d.ts.map +1 -0
- package/dist/core/ecs/compute_backend.d.cts +18 -7
- package/dist/core/ecs/compute_backend.d.ts +18 -7
- package/dist/core/ecs/compute_backend.d.ts.map +1 -1
- package/dist/core/ecs/deferred_commands.d.cts +15 -14
- package/dist/core/ecs/deferred_commands.d.ts +15 -14
- package/dist/core/ecs/deferred_commands.d.ts.map +1 -1
- package/dist/core/ecs/dispatch_trace.d.cts +32 -31
- package/dist/core/ecs/dispatch_trace.d.ts +32 -31
- package/dist/core/ecs/dispatch_trace.d.ts.map +1 -1
- package/dist/core/ecs/ecs.d.cts +289 -270
- package/dist/core/ecs/ecs.d.ts +289 -270
- package/dist/core/ecs/ecs.d.ts.map +1 -1
- package/dist/core/ecs/ecs_memory.d.cts +134 -136
- package/dist/core/ecs/ecs_memory.d.ts +134 -136
- package/dist/core/ecs/ecs_memory.d.ts.map +1 -1
- package/dist/core/ecs/ecs_options.d.cts +89 -0
- package/dist/core/ecs/ecs_options.d.ts +89 -0
- package/dist/core/ecs/ecs_options.d.ts.map +1 -0
- package/dist/core/ecs/entity.d.cts +4 -4
- package/dist/core/ecs/entity.d.ts +4 -4
- package/dist/core/ecs/entity_allocator.d.cts +20 -18
- package/dist/core/ecs/entity_allocator.d.ts +20 -18
- package/dist/core/ecs/entity_allocator.d.ts.map +1 -1
- package/dist/core/ecs/event.d.cts +36 -22
- package/dist/core/ecs/event.d.ts +36 -22
- package/dist/core/ecs/event.d.ts.map +1 -1
- package/dist/core/ecs/facades.d.cts +12 -126
- package/dist/core/ecs/facades.d.ts +12 -126
- package/dist/core/ecs/facades.d.ts.map +1 -1
- package/dist/core/ecs/frame_stepper.d.cts +12 -12
- package/dist/core/ecs/frame_stepper.d.ts +12 -12
- package/dist/core/ecs/frame_stepper.d.ts.map +1 -1
- package/dist/core/ecs/frame_trace.d.cts +32 -32
- package/dist/core/ecs/frame_trace.d.ts +32 -32
- package/dist/core/ecs/frame_trace.d.ts.map +1 -1
- package/dist/core/ecs/host_commands.d.cts +103 -85
- package/dist/core/ecs/host_commands.d.ts +103 -85
- package/dist/core/ecs/host_commands.d.ts.map +1 -1
- package/dist/core/ecs/index.d.cts +21 -11
- package/dist/core/ecs/index.d.ts +21 -11
- package/dist/core/ecs/index.d.ts.map +1 -1
- package/dist/core/ecs/observer.d.cts +39 -120
- package/dist/core/ecs/observer.d.ts +39 -120
- package/dist/core/ecs/observer.d.ts.map +1 -1
- package/dist/core/ecs/phase.d.cts +79 -0
- package/dist/core/ecs/phase.d.ts +79 -0
- package/dist/core/ecs/phase.d.ts.map +1 -0
- package/dist/core/ecs/plugin.d.cts +125 -0
- package/dist/core/ecs/plugin.d.ts +125 -0
- package/dist/core/ecs/plugin.d.ts.map +1 -0
- package/dist/core/ecs/plugin_slots.d.cts +26 -0
- package/dist/core/ecs/plugin_slots.d.ts +26 -0
- package/dist/core/ecs/plugin_slots.d.ts.map +1 -0
- package/dist/core/ecs/query.d.cts +208 -578
- package/dist/core/ecs/query.d.ts +208 -578
- package/dist/core/ecs/query.d.ts.map +1 -1
- package/dist/core/ecs/query_cache.d.cts +130 -0
- package/dist/core/ecs/query_cache.d.ts +130 -0
- package/dist/core/ecs/query_cache.d.ts.map +1 -0
- package/dist/core/ecs/query_registry.d.cts +47 -0
- package/dist/core/ecs/query_registry.d.ts +47 -0
- package/dist/core/ecs/query_registry.d.ts.map +1 -0
- package/dist/core/ecs/query_terms.d.cts +126 -0
- package/dist/core/ecs/query_terms.d.ts +126 -0
- package/dist/core/ecs/query_terms.d.ts.map +1 -0
- package/dist/core/ecs/ref.d.cts +100 -39
- package/dist/core/ecs/ref.d.ts +100 -39
- package/dist/core/ecs/ref.d.ts.map +1 -1
- package/dist/core/ecs/relation.d.cts +101 -182
- package/dist/core/ecs/relation.d.ts +101 -182
- package/dist/core/ecs/relation.d.ts.map +1 -1
- package/dist/core/ecs/resource.d.cts +13 -13
- package/dist/core/ecs/resource.d.ts +13 -13
- package/dist/core/ecs/resource_registry.d.cts +5 -5
- package/dist/core/ecs/resource_registry.d.ts +5 -5
- package/dist/core/ecs/resource_registry.d.ts.map +1 -1
- package/dist/core/ecs/row_kinds.d.cts +24 -0
- package/dist/core/ecs/row_kinds.d.ts +24 -0
- package/dist/core/ecs/row_kinds.d.ts.map +1 -0
- package/dist/core/ecs/run_condition.d.cts +23 -23
- package/dist/core/ecs/run_condition.d.ts +23 -23
- package/dist/core/ecs/run_condition.d.ts.map +1 -1
- package/dist/core/ecs/schedule.d.cts +91 -120
- package/dist/core/ecs/schedule.d.ts +91 -120
- package/dist/core/ecs/schedule.d.ts.map +1 -1
- package/dist/core/ecs/schedule_plan.d.cts +33 -0
- package/dist/core/ecs/schedule_plan.d.ts +33 -0
- package/dist/core/ecs/schedule_plan.d.ts.map +1 -0
- package/dist/core/ecs/snapshot.d.cts +77 -0
- package/dist/core/ecs/snapshot.d.ts +77 -0
- package/dist/core/ecs/snapshot.d.ts.map +1 -0
- package/dist/core/ecs/snapshot_mount.d.cts +42 -0
- package/dist/core/ecs/snapshot_mount.d.ts +42 -0
- package/dist/core/ecs/snapshot_mount.d.ts.map +1 -0
- package/dist/core/ecs/sparse_store.d.cts +108 -51
- package/dist/core/ecs/sparse_store.d.ts +108 -51
- package/dist/core/ecs/sparse_store.d.ts.map +1 -1
- package/dist/core/ecs/store.d.cts +527 -473
- package/dist/core/ecs/store.d.ts +527 -473
- package/dist/core/ecs/store.d.ts.map +1 -1
- package/dist/core/ecs/store_layout_listener.d.cts +6 -6
- package/dist/core/ecs/store_layout_listener.d.ts +6 -6
- package/dist/core/ecs/store_types.d.cts +122 -0
- package/dist/core/ecs/store_types.d.ts +122 -0
- package/dist/core/ecs/store_types.d.ts.map +1 -0
- package/dist/core/ecs/system.d.cts +146 -61
- package/dist/core/ecs/system.d.ts +146 -61
- package/dist/core/ecs/system.d.ts.map +1 -1
- package/dist/core/ecs/system_context.d.cts +296 -0
- package/dist/core/ecs/system_context.d.ts +296 -0
- package/dist/core/ecs/system_context.d.ts.map +1 -0
- package/dist/core/ecs/system_set.d.cts +48 -0
- package/dist/core/ecs/system_set.d.ts +48 -0
- package/dist/core/ecs/system_set.d.ts.map +1 -0
- package/dist/core/ecs/utils/arrays.d.cts +15 -1
- package/dist/core/ecs/utils/arrays.d.ts +15 -1
- package/dist/core/ecs/utils/arrays.d.ts.map +1 -1
- package/dist/core/ecs/utils/error.d.cts +82 -6
- package/dist/core/ecs/utils/error.d.ts +82 -6
- package/dist/core/ecs/utils/error.d.ts.map +1 -1
- package/dist/core/ecs/utils/plugin_error.d.cts +10 -0
- package/dist/core/ecs/utils/plugin_error.d.ts +10 -0
- package/dist/core/ecs/utils/plugin_error.d.ts.map +1 -0
- package/dist/core/restore_errors.d.cts +30 -0
- package/dist/core/restore_errors.d.ts +30 -0
- package/dist/core/restore_errors.d.ts.map +1 -0
- package/dist/core/store/action_ring.d.cts +44 -44
- package/dist/core/store/action_ring.d.ts +44 -44
- package/dist/core/store/action_ring.d.ts.map +1 -1
- package/dist/core/store/allocator.d.cts +149 -95
- package/dist/core/store/allocator.d.ts +149 -95
- package/dist/core/store/allocator.d.ts.map +1 -1
- package/dist/core/store/buffer_backed_column.d.cts +17 -17
- package/dist/core/store/buffer_backed_column.d.ts +17 -17
- package/dist/core/store/buffer_backed_column.d.ts.map +1 -1
- package/dist/core/store/column_store.d.cts +124 -86
- package/dist/core/store/column_store.d.ts +124 -86
- package/dist/core/store/column_store.d.ts.map +1 -1
- package/dist/core/store/command_dispatch.d.cts +14 -15
- package/dist/core/store/command_dispatch.d.ts +14 -15
- package/dist/core/store/command_dispatch.d.ts.map +1 -1
- package/dist/core/store/command_ring.d.cts +36 -39
- package/dist/core/store/command_ring.d.ts +36 -39
- package/dist/core/store/command_ring.d.ts.map +1 -1
- package/dist/core/store/descriptor.d.cts +34 -17
- package/dist/core/store/descriptor.d.ts +34 -17
- package/dist/core/store/descriptor.d.ts.map +1 -1
- package/dist/core/store/entity_index.d.cts +34 -35
- package/dist/core/store/entity_index.d.ts +34 -35
- package/dist/core/store/entity_index.d.ts.map +1 -1
- package/dist/core/store/event_ring.d.cts +36 -38
- package/dist/core/store/event_ring.d.ts +36 -38
- package/dist/core/store/event_ring.d.ts.map +1 -1
- package/dist/core/store/extend.d.cts +13 -7
- package/dist/core/store/extend.d.ts +13 -7
- package/dist/core/store/extend.d.ts.map +1 -1
- package/dist/core/store/grow.d.cts +2 -2
- package/dist/core/store/grow.d.ts +2 -2
- package/dist/core/store/grow.d.ts.map +1 -1
- package/dist/core/store/header.d.cts +31 -27
- package/dist/core/store/header.d.ts +31 -27
- package/dist/core/store/header.d.ts.map +1 -1
- package/dist/core/store/index.d.cts +10 -10
- package/dist/core/store/index.d.ts +10 -10
- package/dist/core/store/index.d.ts.map +1 -1
- package/dist/core/store/layout_ops.d.cts +71 -62
- package/dist/core/store/layout_ops.d.ts +71 -62
- package/dist/core/store/layout_ops.d.ts.map +1 -1
- package/dist/core/store/region_table.d.cts +28 -21
- package/dist/core/store/region_table.d.ts +28 -21
- package/dist/core/store/region_table.d.ts.map +1 -1
- package/dist/core/store/snapshot.d.cts +31 -16
- package/dist/core/store/snapshot.d.ts +31 -16
- package/dist/core/store/snapshot.d.ts.map +1 -1
- package/dist/core/store/state_hash.d.cts +12 -12
- package/dist/core/store/state_hash.d.ts +12 -12
- package/dist/core/store/state_hash.d.ts.map +1 -1
- package/dist/core/store/store_regions.d.cts +10 -10
- package/dist/core/store/store_regions.d.ts +10 -10
- package/dist/core/store/store_regions.d.ts.map +1 -1
- package/dist/core/store/vendored_abi/abi.d.cts +27 -2
- package/dist/core/store/vendored_abi/abi.d.ts +27 -2
- package/dist/core/store/vendored_abi/abi.d.ts.map +1 -1
- package/dist/host_commands-B2KMx8O2.cjs +1 -0
- package/dist/host_commands-ClVPvhSa.cjs +2 -0
- package/dist/host_commands-CwxVQS1z.js +1524 -0
- package/dist/host_commands-nJvTy88E.js +1667 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +15 -10
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.development.cjs +1 -1
- package/dist/index.development.js +5804 -5203
- package/dist/index.js +5323 -4764
- package/dist/internal.cjs +2 -2
- package/dist/internal.d.cts +5 -2
- package/dist/internal.d.ts +5 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.development.cjs +1 -1
- package/dist/internal.development.js +70 -20
- package/dist/internal.js +138 -83
- package/dist/plugins/abi-D3V-QUSf.cjs +1 -0
- package/dist/plugins/abi-NP8pNxpd.js +42 -0
- package/dist/plugins/arrays-Dr_1hCjL.js +28 -0
- package/dist/plugins/arrays-Dt-Joc4i.cjs +1 -0
- package/dist/plugins/assertions-gtDVfbnQ.js +6 -0
- package/dist/plugins/assertions-z5GQd_bB.cjs +1 -0
- package/dist/{extensions → plugins}/editor/editor.d.cts +54 -54
- package/dist/{extensions → plugins}/editor/editor.d.ts +54 -54
- package/dist/plugins/editor/editor.d.ts.map +1 -0
- package/dist/{extensions → plugins}/editor/field_handle.d.cts +12 -12
- package/dist/{extensions → plugins}/editor/field_handle.d.ts +12 -12
- package/dist/plugins/editor/field_handle.d.ts.map +1 -0
- package/dist/plugins/editor/index.d.cts +22 -0
- package/dist/plugins/editor/index.d.ts +22 -0
- package/dist/plugins/editor/index.d.ts.map +1 -0
- package/dist/plugins/editor.cjs +1 -0
- package/dist/plugins/editor.d.cts +1 -0
- package/dist/plugins/editor.d.ts +1 -0
- package/dist/plugins/editor.development.cjs +1 -0
- package/dist/{extensions/editor/index.development.js → plugins/editor.development.js} +81 -64
- package/dist/{extensions/editor/index.js → plugins/editor.js} +81 -64
- package/dist/plugins/events/event_channel.d.cts +13 -0
- package/dist/plugins/events/event_channel.d.ts +13 -0
- package/dist/plugins/events/event_channel.d.ts.map +1 -0
- package/dist/plugins/events/event_registry.d.cts +22 -0
- package/dist/plugins/events/event_registry.d.ts +22 -0
- package/dist/plugins/events/event_registry.d.ts.map +1 -0
- package/dist/plugins/events/facade.d.cts +26 -0
- package/dist/plugins/events/facade.d.ts +26 -0
- package/dist/plugins/events/facade.d.ts.map +1 -0
- package/dist/plugins/events/index.d.cts +10 -0
- package/dist/plugins/events/index.d.ts +10 -0
- package/dist/plugins/events/index.d.ts.map +1 -0
- package/dist/plugins/events.cjs +1 -0
- package/dist/plugins/events.d.cts +1 -0
- package/dist/plugins/events.d.ts +1 -0
- package/dist/plugins/events.development.cjs +1 -0
- package/dist/plugins/events.development.js +174 -0
- package/dist/plugins/events.js +159 -0
- package/dist/plugins/observers/index.d.cts +23 -0
- package/dist/plugins/observers/index.d.ts +23 -0
- package/dist/plugins/observers/index.d.ts.map +1 -0
- package/dist/plugins/observers/observer_registry.d.cts +101 -0
- package/dist/plugins/observers/observer_registry.d.ts +101 -0
- package/dist/plugins/observers/observer_registry.d.ts.map +1 -0
- package/dist/plugins/observers.cjs +1 -0
- package/dist/plugins/observers.d.cts +1 -0
- package/dist/plugins/observers.d.ts +1 -0
- package/dist/plugins/observers.development.cjs +1 -0
- package/dist/plugins/observers.development.js +431 -0
- package/dist/plugins/observers.js +403 -0
- package/dist/plugins/relation_store-BsoegrZh.js +313 -0
- package/dist/plugins/relation_store-DQKSnxsm.cjs +1 -0
- package/dist/plugins/relation_store-RBhpcE28.cjs +1 -0
- package/dist/plugins/relation_store-RqW5Snx6.js +328 -0
- package/dist/plugins/relations/builtin_relations.d.cts +48 -0
- package/dist/plugins/relations/builtin_relations.d.ts +48 -0
- package/dist/plugins/relations/builtin_relations.d.ts.map +1 -0
- package/dist/plugins/relations/facade.d.cts +73 -0
- package/dist/plugins/relations/facade.d.ts +73 -0
- package/dist/plugins/relations/facade.d.ts.map +1 -0
- package/dist/plugins/relations/index.d.cts +11 -0
- package/dist/plugins/relations/index.d.ts +11 -0
- package/dist/plugins/relations/index.d.ts.map +1 -0
- package/dist/{core/ecs → plugins/relations}/relation_service.d.cts +85 -97
- package/dist/{core/ecs → plugins/relations}/relation_service.d.ts +85 -97
- package/dist/plugins/relations/relation_service.d.ts.map +1 -0
- package/dist/plugins/relations/relation_store.d.cts +177 -0
- package/dist/plugins/relations/relation_store.d.ts +177 -0
- package/dist/plugins/relations/relation_store.d.ts.map +1 -0
- package/dist/plugins/relations.cjs +1 -0
- package/dist/plugins/relations.d.cts +1 -0
- package/dist/plugins/relations.d.ts +1 -0
- package/dist/plugins/relations.development.cjs +1 -0
- package/dist/plugins/relations.development.js +603 -0
- package/dist/plugins/relations.js +553 -0
- package/dist/plugins/snapshots/index.d.cts +33 -0
- package/dist/plugins/snapshots/index.d.ts +33 -0
- package/dist/plugins/snapshots/index.d.ts.map +1 -0
- package/dist/plugins/snapshots/resume.d.cts +57 -0
- package/dist/plugins/snapshots/resume.d.ts +57 -0
- package/dist/plugins/snapshots/resume.d.ts.map +1 -0
- package/dist/plugins/snapshots/snapshot_service.d.cts +59 -0
- package/dist/plugins/snapshots/snapshot_service.d.ts +59 -0
- package/dist/plugins/snapshots/snapshot_service.d.ts.map +1 -0
- package/dist/plugins/snapshots.cjs +1 -0
- package/dist/plugins/snapshots.d.cts +1 -0
- package/dist/plugins/snapshots.d.ts +1 -0
- package/dist/plugins/snapshots.development.cjs +1 -0
- package/dist/plugins/snapshots.development.js +737 -0
- package/dist/plugins/snapshots.js +737 -0
- package/dist/plugins/solid/index.d.cts +8 -0
- package/dist/plugins/solid/index.d.ts +8 -0
- package/dist/plugins/solid/index.d.ts.map +1 -0
- package/dist/plugins/solid/solid.d.cts +97 -0
- package/dist/plugins/solid/solid.d.ts +97 -0
- package/dist/plugins/solid/solid.d.ts.map +1 -0
- package/dist/plugins/solid.cjs +1 -0
- package/dist/plugins/solid.d.cts +1 -0
- package/dist/plugins/solid.d.ts +1 -0
- package/dist/plugins/solid.development.cjs +1 -0
- package/dist/plugins/solid.development.js +319 -0
- package/dist/plugins/solid.js +319 -0
- package/dist/plugins/workers/index.d.cts +75 -0
- package/dist/plugins/workers/index.d.ts +75 -0
- package/dist/plugins/workers/index.d.ts.map +1 -0
- package/dist/plugins/workers/node_threads.d.cts +25 -0
- package/dist/plugins/workers/node_threads.d.ts +25 -0
- package/dist/plugins/workers/node_threads.d.ts.map +1 -0
- package/dist/plugins/workers/plan.d.cts +61 -0
- package/dist/plugins/workers/plan.d.ts +61 -0
- package/dist/plugins/workers/plan.d.ts.map +1 -0
- package/dist/plugins/workers/pool.d.cts +144 -0
- package/dist/plugins/workers/pool.d.ts +144 -0
- package/dist/plugins/workers/pool.d.ts.map +1 -0
- package/dist/plugins/workers/protocol.d.cts +112 -0
- package/dist/plugins/workers/protocol.d.ts +112 -0
- package/dist/plugins/workers/protocol.d.ts.map +1 -0
- package/dist/plugins/workers/worker_bind.d.cts +64 -0
- package/dist/plugins/workers/worker_bind.d.ts +64 -0
- package/dist/plugins/workers/worker_bind.d.ts.map +1 -0
- package/dist/plugins/workers/worker_loop.d.cts +14 -0
- package/dist/plugins/workers/worker_loop.d.ts +14 -0
- package/dist/plugins/workers/worker_loop.d.ts.map +1 -0
- package/dist/plugins/workers.cjs +1 -0
- package/dist/plugins/workers.d.cts +1 -0
- package/dist/plugins/workers.d.ts +1 -0
- package/dist/plugins/workers.development.cjs +1 -0
- package/dist/plugins/workers.development.js +596 -0
- package/dist/plugins/workers.js +474 -0
- package/dist/primitives.cjs +1 -1
- package/dist/primitives.d.cts +4 -4
- package/dist/primitives.d.ts +4 -4
- package/dist/primitives.development.cjs +1 -1
- package/dist/primitives.development.js +66 -22
- package/dist/primitives.js +66 -22
- package/dist/shared-BKl12xF1.js +132 -0
- package/dist/shared-Dk-pACZT.cjs +1 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.d.cts +7 -7
- package/dist/shared.d.ts +7 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.development.cjs +1 -1
- package/dist/shared.development.js +4 -3
- package/dist/shared.js +4 -3
- package/dist/topological_sort-Cc_yXqPT.js +346 -0
- package/dist/topological_sort-vp1imU6D.cjs +1 -0
- package/dist/type_primitives/assertions.d.cts +12 -16
- package/dist/type_primitives/assertions.d.ts +12 -16
- package/dist/type_primitives/assertions.d.ts.map +1 -1
- package/dist/type_primitives/binary_heap/binary_heap.d.cts +2 -6
- package/dist/type_primitives/binary_heap/binary_heap.d.ts +2 -6
- package/dist/type_primitives/binary_heap/binary_heap.d.ts.map +1 -1
- package/dist/type_primitives/bitset/bitset.d.cts +7 -11
- package/dist/type_primitives/bitset/bitset.d.ts +7 -11
- package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
- package/dist/type_primitives/brand.d.cts +2 -7
- package/dist/type_primitives/brand.d.ts +2 -7
- package/dist/type_primitives/brand.d.ts.map +1 -1
- package/dist/type_primitives/error.d.cts +1 -2
- package/dist/type_primitives/error.d.ts +1 -2
- package/dist/type_primitives/error.d.ts.map +1 -1
- package/dist/type_primitives/index.d.cts +1 -1
- package/dist/type_primitives/index.d.ts +1 -1
- package/dist/type_primitives/sparse_map/sparse_map.d.cts +3 -7
- package/dist/type_primitives/sparse_map/sparse_map.d.ts +3 -7
- package/dist/type_primitives/sparse_map/sparse_map.d.ts.map +1 -1
- package/dist/type_primitives/sparse_set/sparse_set.d.cts +2 -6
- package/dist/type_primitives/sparse_set/sparse_set.d.ts +2 -6
- package/dist/type_primitives/sparse_set/sparse_set.d.ts.map +1 -1
- package/dist/type_primitives/topological_sort/topological_sort.d.cts +1 -5
- package/dist/type_primitives/topological_sort/topological_sort.d.ts +1 -5
- package/dist/type_primitives/topological_sort/topological_sort.d.ts.map +1 -1
- package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +16 -15
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +16 -15
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
- package/dist/utils/error.d.ts.map +1 -1
- package/dist/version.d.cts +3 -3
- package/dist/version.d.ts +3 -3
- package/dist/worker.cjs +1 -0
- package/dist/worker.d.cts +17 -0
- package/dist/worker.d.ts +17 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.development.cjs +1 -0
- package/dist/worker.development.js +253 -0
- package/dist/worker.js +253 -0
- package/package.json +363 -191
- package/dist/core/ecs/builtin_relations.d.cts +0 -43
- package/dist/core/ecs/builtin_relations.d.ts +0 -43
- package/dist/core/ecs/builtin_relations.d.ts.map +0 -1
- package/dist/core/ecs/event_registry.d.cts +0 -22
- package/dist/core/ecs/event_registry.d.ts +0 -22
- package/dist/core/ecs/event_registry.d.ts.map +0 -1
- package/dist/core/ecs/relation_service.d.ts.map +0 -1
- package/dist/core/ecs/resume.d.cts +0 -85
- package/dist/core/ecs/resume.d.ts +0 -85
- package/dist/core/ecs/resume.d.ts.map +0 -1
- package/dist/core/ecs/snapshot_service.d.cts +0 -94
- package/dist/core/ecs/snapshot_service.d.ts +0 -94
- package/dist/core/ecs/snapshot_service.d.ts.map +0 -1
- package/dist/extensions/editor/editor.d.ts.map +0 -1
- package/dist/extensions/editor/field_handle.d.ts.map +0 -1
- package/dist/extensions/editor/index.cjs +0 -1
- package/dist/extensions/editor/index.d.cts +0 -21
- package/dist/extensions/editor/index.d.ts +0 -21
- package/dist/extensions/editor/index.d.ts.map +0 -1
- package/dist/extensions/editor/index.development.cjs +0 -1
- package/dist/extensions/reactive/ecs_sync.d.cts +0 -214
- package/dist/extensions/reactive/ecs_sync.d.ts +0 -214
- package/dist/extensions/reactive/ecs_sync.d.ts.map +0 -1
- package/dist/extensions/reactive/index.cjs +0 -1
- package/dist/extensions/reactive/index.d.cts +0 -23
- package/dist/extensions/reactive/index.d.ts +0 -23
- package/dist/extensions/reactive/index.d.ts.map +0 -1
- package/dist/extensions/reactive/index.development.cjs +0 -1
- package/dist/extensions/reactive/index.development.js +0 -215
- package/dist/extensions/reactive/index.js +0 -215
- package/dist/extensions/solid/index.cjs +0 -1
- package/dist/extensions/solid/index.d.cts +0 -6
- package/dist/extensions/solid/index.d.ts +0 -6
- package/dist/extensions/solid/index.d.ts.map +0 -1
- package/dist/extensions/solid/index.development.cjs +0 -1
- package/dist/extensions/solid/index.development.js +0 -40
- package/dist/extensions/solid/index.js +0 -36
- package/dist/extensions/solid/kernel_solid.d.cts +0 -45
- package/dist/extensions/solid/kernel_solid.d.ts +0 -45
- package/dist/extensions/solid/kernel_solid.d.ts.map +0 -1
- package/dist/host_commands-BlR3oZ3x.js +0 -2406
- package/dist/host_commands-C2cpU3yC.cjs +0 -1
- package/dist/internal-BoxWl4dG.js +0 -2565
- package/dist/internal-CGBEwfeB.cjs +0 -2
- package/dist/interop-CT-REx0W.cjs +0 -1
- package/dist/interop-CcY6ASQc.js +0 -18
- package/dist/interop-DqxleBo7.js +0 -18
- package/dist/interop-T1HZ-Dpa.cjs +0 -1
- package/dist/kernel-Cz2Kxlwl.js +0 -240
- package/dist/kernel-DgyrLFjW.js +0 -227
- package/dist/kernel-sw7ScKWu.cjs +0 -1
- package/dist/kernel-yWV3XnAb.cjs +0 -1
- package/dist/reactive/array.d.cts +0 -24
- package/dist/reactive/array.d.ts +0 -24
- package/dist/reactive/array.d.ts.map +0 -1
- package/dist/reactive/index.cjs +0 -1
- package/dist/reactive/index.d.cts +0 -11
- package/dist/reactive/index.d.ts +0 -11
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/index.development.cjs +0 -1
- package/dist/reactive/index.development.js +0 -18
- package/dist/reactive/index.js +0 -18
- package/dist/reactive/interop.d.cts +0 -19
- package/dist/reactive/interop.d.ts +0 -19
- package/dist/reactive/interop.d.ts.map +0 -1
- package/dist/reactive/kernel.d.cts +0 -73
- package/dist/reactive/kernel.d.ts +0 -73
- package/dist/reactive/kernel.d.ts.map +0 -1
- package/dist/reactive/map.d.cts +0 -16
- package/dist/reactive/map.d.ts +0 -16
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/shallow.d.cts +0 -11
- package/dist/reactive/shallow.d.ts +0 -11
- package/dist/reactive/shallow.d.ts.map +0 -1
- package/dist/reactive/struct.d.cts +0 -10
- package/dist/reactive/struct.d.ts +0 -10
- package/dist/reactive/struct.d.ts.map +0 -1
- package/dist/shallow-Bs39EPh1.js +0 -150
- package/dist/shallow-C5YmTbtb.cjs +0 -1
- package/dist/shallow-CiFC70Dz.js +0 -159
- package/dist/shallow-DQ8aG51_.cjs +0 -1
- package/dist/shared-B4v0L1cw.js +0 -109
- package/dist/shared-DU78k0pU.cjs +0 -1
- package/dist/topological_sort-BDvEyb9W.cjs +0 -1
- package/dist/topological_sort-DK6EjpWa.js +0 -135
- package/dist/typed_arrays-D3NmwtCG.cjs +0 -1
- package/dist/typed_arrays-DIVwVy5_.js +0 -258
package/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,969 @@ 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.
|
|
8
|
+
## [0.6.0] - 2026-09-08
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### Changed (breaking). One vocabulary for the query connectives
|
|
11
|
+
|
|
12
|
+
The connectives are `and`, `not` and `or`, one word each, everywhere they appear.
|
|
13
|
+
|
|
14
|
+
| 0.5 | 0.6 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `query.without(...)` | `query.not(...)` |
|
|
17
|
+
| `query.anyOf(...)` | `query.or(...)` |
|
|
18
|
+
| `query.withSparse(...)` | `query.andSparse(...)` |
|
|
19
|
+
| `query.withoutSparse(...)` | `query.notSparse(...)` |
|
|
20
|
+
| `query.withRelation(...)` | `query.andRelation(...)` |
|
|
21
|
+
| `query.withoutRelation(...)` | `query.notRelation(...)` |
|
|
22
|
+
| `qb.with(...)`, the builder form | `qb.and(...)` |
|
|
23
|
+
| `changed.without(...)`, `changed.anyOf(...)` | `changed.not(...)`, `changed.or(...)` |
|
|
24
|
+
| `not(cond)`, the run condition | `runIfNot(cond)` |
|
|
25
|
+
| `allOf(...conds)`, the run condition | `runIfAll(...conds)` |
|
|
26
|
+
| `anyOf(...conds)`, the run condition | `runIfAny(...conds)` |
|
|
27
|
+
|
|
28
|
+
`without`, `anyOf` and the four `with*` term verbs spelled the same three ideas in a second
|
|
29
|
+
vocabulary. A sparse term and a relation term keep their own verbs. A sparse id and a relation id
|
|
30
|
+
are both plain numbers at run time. No test tells them apart, so one `and` cannot
|
|
31
|
+
route them. Each now puts the connective word first.
|
|
32
|
+
|
|
33
|
+
The run condition combinators moved into the `runIf` family that `runIfAnyMatch` and
|
|
34
|
+
`runIfResourceEq` already used. That frees the three bare words for the query engine.
|
|
35
|
+
|
|
36
|
+
### Added. `Query.where(term)`, and the `and`, `or` and `not` combinators
|
|
37
|
+
|
|
38
|
+
A chain asks one flat question of the component mask. An expression nests:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { and, or, not } from "@oasys/oecs";
|
|
42
|
+
|
|
43
|
+
const q = ecs.query(Tag).where(or(and(Pos, Vel), Frozen));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each combinator takes a component definition as a leaf, and another expression as a node. It
|
|
47
|
+
returns an `ArchetypeTerm`, which is `{ name, matches }`. A plugin supplies its own term on the
|
|
48
|
+
same footing. The three words are functionally complete over archetype membership, so there is no
|
|
49
|
+
fourth connective. An expression judges the dense component mask alone. Sparse membership and
|
|
50
|
+
relation membership are per entity, outside the archetype, and stay on their own terms.
|
|
51
|
+
|
|
52
|
+
The term runs one time per archetype, per query, at the rebuild the store's dirty epoch triggers.
|
|
53
|
+
`forEach`, `forEachChunk` and `forEachEntity` walk the list the rebuild produced.
|
|
54
|
+
|
|
55
|
+
**Added. `ECS_ERROR.QUERY_TERM_DENSE_PATH`.** `archetypeCount`, `archetypes` and `excludeWords`
|
|
56
|
+
answer from the unfiltered archetype list. So a query that carries an archetype term refuses all
|
|
57
|
+
three in a development build. The refusal used to borrow `SPARSE_QUERY_DENSE_PATH`, which named
|
|
58
|
+
the wrong fault.
|
|
59
|
+
|
|
60
|
+
**Added. The `ArchetypeExpr` type**, the operand of a combinator.
|
|
61
|
+
|
|
62
|
+
### Changed (breaking). Four subsystems became plugins a world installs
|
|
63
|
+
|
|
64
|
+
`new ECS()` no longer carries relations, events, snapshot and restore, or observers. Each is a
|
|
65
|
+
plugin on its own subpath, installed at construction:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { ECS } from "@oasys/oecs";
|
|
69
|
+
import { relations } from "@oasys/oecs/relations";
|
|
70
|
+
import { observers } from "@oasys/oecs/observers";
|
|
71
|
+
|
|
72
|
+
const world = ECS.create({ plugins: [relations(), observers()] });
|
|
73
|
+
world.relations.register();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`ECS.create` returns the world intersected with the facades its plugins contribute. A world that
|
|
77
|
+
did not install a plugin has no member to reach for. So `ecs.relations` on a bare world is a
|
|
78
|
+
compile error, and not a fault at run time. `new ECS()` still builds a world, and that world holds
|
|
79
|
+
none of the four.
|
|
80
|
+
|
|
81
|
+
The reason is that a class method cannot be removed by a bundler. While `ECS` declared `relations`
|
|
82
|
+
and `snapshots`, every program carried the relation and snapshot code whether or not it named them.
|
|
83
|
+
A plugin the construction site imports is a reference a bundler can follow, and one it can drop.
|
|
84
|
+
A program that installs none of the four now ships far less code. `bench/` holds the measurement.
|
|
85
|
+
|
|
86
|
+
Each plugin keeps its call sites unchanged. Only construction moves.
|
|
87
|
+
|
|
88
|
+
- `@oasys/oecs/relations`, `relations()`, gives `ecs.relations` and the relation terms on a query.
|
|
89
|
+
- `@oasys/oecs/events`, `events()`, gives `ecs.events`, `ctx.emit` and `ctx.readEvents`.
|
|
90
|
+
- `@oasys/oecs/snapshots`, `snapshots()`, gives `ecs.snapshots.capture` and `.restore`.
|
|
91
|
+
- `@oasys/oecs/observers`, `observers()`, gives `ecs.observe`.
|
|
92
|
+
|
|
93
|
+
On npm, each plugin also has a `/dev` subpath. `@oasys/oecs/relations/dev` and the seven others,
|
|
94
|
+
`@oasys/oecs/editor/dev` and `@oasys/oecs/solid/dev` among them, serve the build with the
|
|
95
|
+
development guards on. JSR publishes no `/dev` subpath. A plugin binds to the core build it was
|
|
96
|
+
made against. Take the plugin and the world from the same channel.
|
|
97
|
+
|
|
98
|
+
`ecs.snapshots.stateHash()` and `ecs.snapshots.deterministic` stay on every world. They describe the
|
|
99
|
+
world, not the plugin, and the determinism opt-in is still separate: `capture` and `restore`
|
|
100
|
+
throw `DETERMINISM_DISABLED` on a world built without `{ deterministic: true }`, installed or not.
|
|
101
|
+
|
|
102
|
+
In TypeScript, reaching for a plugin the world did not install is a compile error. In JavaScript
|
|
103
|
+
nothing stops the call, so the world throws `ECS_ERROR.PLUGIN_NOT_INSTALLED`. The message names
|
|
104
|
+
the API and the import that supplies it. The fix is at the construction site. On a bare world every
|
|
105
|
+
member of `ecs.relations` and of `ecs.events` throws it. So do the call `ecs.observe(...)` and the
|
|
106
|
+
four members `ecs.snapshots.capture`, `restore`, `captureSparse` and `restoreSparse`. The
|
|
107
|
+
system-side seams throw it too. `ctx.emit`, `ctx.readEvents`, `ctx.addRelation`,
|
|
108
|
+
`query.andRelation`, `query.hierarchy` and `query.forEachRelatedTo` are among them.
|
|
109
|
+
|
|
110
|
+
Installing one plugin two times throws the new `ECS_ERROR.PLUGIN_ALREADY_INSTALLED`.
|
|
111
|
+
|
|
112
|
+
The types `Plugin`, `PluginHost` and `PluginsOf` are exported from `@oasys/oecs`. A
|
|
113
|
+
third-party plugin is typed the way the four built-in plugins are. `Plugin<X>` is what a
|
|
114
|
+
factory such as `relations()` returns, and what a plugin list holds. Its `install` takes a
|
|
115
|
+
`PluginHost` and returns `X`, the surface the world gains. `PluginsOf` is the surface a plugin
|
|
116
|
+
list adds to the world.
|
|
117
|
+
|
|
118
|
+
### Changed (breaking). `registerIsA` and `registerChildOf` ship on the relations entry
|
|
119
|
+
|
|
120
|
+
The two built-in relation presets moved off the package root:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// before
|
|
124
|
+
import { ECS, registerChildOf } from "@oasys/oecs";
|
|
125
|
+
|
|
126
|
+
// after
|
|
127
|
+
import { ECS } from "@oasys/oecs";
|
|
128
|
+
import { relations, registerChildOf } from "@oasys/oecs/relations";
|
|
129
|
+
|
|
130
|
+
const world = ECS.create({ plugins: [relations()] });
|
|
131
|
+
const ChildOf = registerChildOf(world);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`BuiltinRelationOptions` moved with them. Each function calls
|
|
135
|
+
`world.relations.register`, so it needs the plugin, and a bare world cannot call it. Exporting
|
|
136
|
+
them from the root also pulled the relation code into every bundle. The plugin split set out to
|
|
137
|
+
stop that.
|
|
138
|
+
|
|
139
|
+
### Added. A change feed more than one plugin reads, and a richer plugin host
|
|
140
|
+
|
|
141
|
+
The store's record of what changed is now a seam any plugin drains. `ChangeFeed` names it,
|
|
142
|
+
`Store` implements it, and `PluginHost.changes` hands it out. A consumer asks for a grain with
|
|
143
|
+
`configureObservation` or `configureSparseObservation`, keyed by its plugin name. It drains with
|
|
144
|
+
`drainSet` or `drainSparseSet`. The store merges every consumer's ask by OR. So one consumer dropping
|
|
145
|
+
a flag never takes that flag from another. Each drain is memoized on its run. A second consumer of
|
|
146
|
+
one run gets the result the first one got. A consumer also takes each structural round through
|
|
147
|
+
`addStructuralHook`, and the feed carries `forEachChangedArchetype`, `collectEnabledWith`, `isAlive`,
|
|
148
|
+
`isDisabled` and `hasComponent`.
|
|
149
|
+
|
|
150
|
+
`PluginHost` gains three members. `host.world` is the bare world. Take it to register a system,
|
|
151
|
+
read a field, build a cursor or reach a resource. `host.changes` is the change feed.
|
|
152
|
+
`host.onSettle(fn)` runs `fn` at the tail of every `update()`, after every system and every flush of
|
|
153
|
+
the frame. Hooks run in install order. The `run` argument sits above every stamp the frame made.
|
|
154
|
+
|
|
155
|
+
`Plugin` gains `requires`, the plugins this one reads through, by name. `ECS.create` walks
|
|
156
|
+
the plugin list in order, so a dependency comes earlier in the list. A missing one throws
|
|
157
|
+
`PLUGIN_NOT_INSTALLED` at construction, and the message names the plugin that asked.
|
|
158
|
+
|
|
159
|
+
`ECS.create` now checks the facade a plugin returns. A key that names a member the world already
|
|
160
|
+
carries throws the new `ECS_ERROR.PLUGIN_SURFACE_COLLISION`. `Object.assign` would overwrite that
|
|
161
|
+
member without a word. The five reserved slots, `relations`, `events`, `observe`, `snapshots` and
|
|
162
|
+
`workers`, are the exception. The check is development-only.
|
|
163
|
+
|
|
164
|
+
Four types are exported from `@oasys/oecs`: `ChangeFeed`, `ObservationFlags`, `DrainResult` and
|
|
165
|
+
`StructuralObserverEvents`. The new [plugins](docs/api/plugins.md) page documents the host,
|
|
166
|
+
the rules and the feed for an author.
|
|
167
|
+
|
|
168
|
+
### Added. The phase set is open
|
|
169
|
+
|
|
170
|
+
`ecs.addPhase(name, { loop, before, after })` adds one slot to the schedule and gives back a
|
|
171
|
+
`Phase` handle. A plugin owns its own slot that way. It no longer contends for insertion order
|
|
172
|
+
inside a phase the application also writes to.
|
|
173
|
+
|
|
174
|
+
`loop` is `"startup"`, `"fixed"` or `"update"`. It decides which drive runs the phase, and what
|
|
175
|
+
delta time the phase receives. `before` and `after` order the new phase against the other phases of
|
|
176
|
+
the same loop, built-in or added. A target in another loop expands to nothing. A phase that names
|
|
177
|
+
no neighbour lands at the tail of its loop. Each loop's phases sort with Kahn's algorithm, and
|
|
178
|
+
declaration order breaks a tie. The systems inside a phase already follow that rule. The order
|
|
179
|
+
resolves at `addPhase`.
|
|
180
|
+
|
|
181
|
+
A phase has an identity of object identity, and not of name, the rule `systemSet` follows. Two
|
|
182
|
+
calls with one name make two phases. The handle belongs to the world that made it.
|
|
183
|
+
|
|
184
|
+
The seven built-ins keep their `SCHEDULE` spelling and gain no handle. `addSystems` takes either
|
|
185
|
+
spelling, so no call that you write today changes.
|
|
186
|
+
|
|
187
|
+
`Phase`, `PhaseConfig`, `PhaseLoop`, `PhaseName` and `SchedulePhase` are exported from
|
|
188
|
+
`@oasys/oecs`. Two error categories are new. `ECS_ERROR.UNKNOWN_PHASE` names a phase this world
|
|
189
|
+
does not hold: a name no built-in spells, or a handle another world made.
|
|
190
|
+
`ECS_ERROR.CIRCULAR_PHASE_DEPENDENCY` names a phase order with a cycle. Both throw in each build.
|
|
191
|
+
A handle from another world would otherwise push systems into that world's list. A production
|
|
192
|
+
build would then run them nowhere.
|
|
193
|
+
|
|
194
|
+
A plugin adds a phase through `host.world.addPhase`.
|
|
195
|
+
`src/core/ecs/__tests__/integration/phase.test.ts` locks the order against the built-ins a phase
|
|
196
|
+
names, the identity rule, the three loops and both faults. See
|
|
197
|
+
[schedule](docs/api/schedule.md).
|
|
198
|
+
|
|
199
|
+
### Changed (breaking). A frame trace event carries a `PhaseName`
|
|
200
|
+
|
|
201
|
+
`FrameTraceSink` and `FrameTraceEvent` widen `phase` from `SCHEDULE` to `PhaseName`, which is
|
|
202
|
+
`SCHEDULE | (string & {})`. A phase from `addPhase` spells the name it was given, so the value set
|
|
203
|
+
is open. A consumer that switches on `SCHEDULE.UPDATE` keeps working, and it now needs a default
|
|
204
|
+
arm. A consumer that assigns `event.phase` to a `SCHEDULE` variable no longer compiles. On
|
|
205
|
+
`FrameTraceSink`, `systemBegin`, `flushBegin`, `flushEnd` and `phaseBoundary` each take a
|
|
206
|
+
`PhaseName`.
|
|
207
|
+
|
|
208
|
+
### Changed. The frame loop reads a phase's plan from a field
|
|
209
|
+
|
|
210
|
+
Each loop holds its phases as one sorted array, resolved at `addPhase`. Each phase holds its own
|
|
211
|
+
sorted plan as a field. The drive loop walks the array and reads the field. It used to key a map by
|
|
212
|
+
the phase string, once for each phase, in each frame. A schedule of short system bodies gets
|
|
213
|
+
faster. That is the case where the scheduler's own work is most of the frame. `bench/` holds the
|
|
214
|
+
comparison.
|
|
215
|
+
|
|
216
|
+
### Changed (breaking for a plugin author). Every host hook point names what it hooks
|
|
217
|
+
|
|
218
|
+
`PluginHost` named two plugins. It now carries nine members, and every one is open to any plugin:
|
|
219
|
+
`store`, `world`, `changes`, `context`, `memory`, `onSettle`, `onPrewarm`, `onDispose` and
|
|
220
|
+
`installRoute`.
|
|
221
|
+
|
|
222
|
+
- `onPrewarm(fn)` contributes the access shapes that `startup()` folds into the archetype closure.
|
|
223
|
+
The observers plugin takes it, where it used to hand the world a registry.
|
|
224
|
+
- `onDispose(fn)` runs when the world goes away. A plugin ends a thread, a timer or a socket there.
|
|
225
|
+
- `memory` names where the world's bytes are: `backing`, `backingSource` and `storeBase`.
|
|
226
|
+
- `installRoute(planner)` claims the body of the systems a plugin routes. It gives back the one
|
|
227
|
+
call that turns the route on and off. A world holds one route, so a second install throws
|
|
228
|
+
`PLUGIN_ALREADY_INSTALLED`.
|
|
229
|
+
|
|
230
|
+
The renames that go with it: `WorkerHooks` is `SystemRoutePlanner`, `WorkerWorld` splits into
|
|
231
|
+
`PluginMemory` and `RouteControl`, `ParallelRoute` is `RouteDispatch`,
|
|
232
|
+
`SystemDescriptor.parallelPlan` is `routePlan`, and `Schedule.setWorkerPool` is `setRoute`. Of
|
|
233
|
+
these, `PluginHost` alone was ever exported, so the published break reaches a plugin author and
|
|
234
|
+
nobody else. An application that installs plugins sees nothing.
|
|
235
|
+
|
|
236
|
+
Two rules changed with the rename. The world asks the planner about every system it registers.
|
|
237
|
+
A planner answers `undefined` for one it does not claim. The old hook saw only a system
|
|
238
|
+
carrying a `parallel` config. So a plugin now validates its own config for every candidate.
|
|
239
|
+
`noteScan` moved onto `ChangeFeed`, where a consumer of the feed reaches it.
|
|
240
|
+
|
|
241
|
+
The dispatch path is unchanged by construction. Registration is keyed and cold, and the world
|
|
242
|
+
caches what it resolved into the field the dispatch already read. The emitted production dispatch
|
|
243
|
+
loop is byte-identical once the rename is undone.
|
|
244
|
+
|
|
245
|
+
`src/core/ecs/__tests__/integration/third_party_plugin.test.ts` writes a route outside the core.
|
|
246
|
+
It claims one system body, runs a dispose hook, and holds the world to the second-route refusal. See
|
|
247
|
+
[plugins](docs/api/plugins.md).
|
|
248
|
+
|
|
249
|
+
### Removed (breaking for a plugin author). `installObservers` and `installWorkers`
|
|
250
|
+
|
|
251
|
+
Both were named slots on `PluginHost`, one for each first-party plugin. The core named two plugins
|
|
252
|
+
that way, and every other plugin went through the generic path. The generic hook points above
|
|
253
|
+
replace them. A plugin outside this package now reaches the ground the first parties reach.
|
|
254
|
+
|
|
255
|
+
The stated reason for the two slots was hot-path lookup cost. It holds for one read alone, the
|
|
256
|
+
system dispatch route, and that read stays one typed slot. Every other read behind the two slots is
|
|
257
|
+
cold.
|
|
258
|
+
|
|
259
|
+
### Added. `solid()`, a plugin that writes one Solid signal per row off the change feed
|
|
260
|
+
|
|
261
|
+
`@oasys/oecs/solid` now exports `solid()`, for `ECS.create({ plugins: [solid()] })`. A world that
|
|
262
|
+
installs it carries `ecs.solid`, which projects ECS state into Solid signals. It reads the change
|
|
263
|
+
feed and writes Solid, with nothing in between. The observers plugin is out of that path. This is
|
|
264
|
+
the one path from ECS state into a UI.
|
|
265
|
+
|
|
266
|
+
`ecs.solid` has three entry points, and each view carries `dispose()`. `component(def, project)`
|
|
267
|
+
projects one dense component, and `cell(id)` is that row's value as one Solid signal. The first call
|
|
268
|
+
for an id makes the signal, and every later call returns the same accessor. So bind it once for each
|
|
269
|
+
row. A `keys()` signal beside it drives a keyed `<For>`. `fields(def, fields)` is sugar that
|
|
270
|
+
publishes a fixed field list as a record, with an `eq` that compares those fields.
|
|
271
|
+
`singleton(def, eid, fields)` publishes one entity's fields into a keyless Solid store, where a fixed
|
|
272
|
+
key set earns the store's cost. A remove or a disable of the target resets them. `grain` is
|
|
273
|
+
`"entity"`, the default, or `"column"`. `eq` is each cell's value equality, handed to Solid as the
|
|
274
|
+
signal's `equals`, and it defaults to Solid's `===`. `seedExisting` publishes the current enabled
|
|
275
|
+
members at creation, and defaults to true.
|
|
276
|
+
|
|
277
|
+
The first design published into a Solid store keyed by entity id. `bench/foundations/p23-solid.mjs`
|
|
278
|
+
measured that store's publish above the publish that ships, at every density. So a row became a
|
|
279
|
+
signal before this release.
|
|
280
|
+
|
|
281
|
+
Everything publishes at the settle point, the tail of `update()`. A structural event arrives mid-tick
|
|
282
|
+
and records an entity id. Nothing reaches Solid inside the flush. The plugin then publishes
|
|
283
|
+
inside one Solid `batch`. One `update()` is one Solid flush, whatever the number of views. An entity
|
|
284
|
+
spawned and despawned in one tick never appears. A published value is the final value of the tick.
|
|
285
|
+
Only a deferred structural operation reaches a view. The observers carry the same limit, because both
|
|
286
|
+
read structural events from one flush.
|
|
287
|
+
|
|
288
|
+
This plugin and the observers plugin are two consumers of one feed. Install both, in either
|
|
289
|
+
order, and each one sees the same by-id write.
|
|
290
|
+
|
|
291
|
+
What it refuses. Dense components only. A sparse definition throws a `TypeError` that names the call.
|
|
292
|
+
No join. A view subscribes to one component, so a projection that reads a second component goes
|
|
293
|
+
stale. An entity that leaves the component and rejoins inside one tick projects twice. The value is
|
|
294
|
+
the final one. A cell carries the whole projected value, so a field read does not track that field
|
|
295
|
+
alone. A projection must not return a function, which a Solid setter reads as an updater. A cell is
|
|
296
|
+
kept for the life of the view.
|
|
297
|
+
|
|
298
|
+
Measured against the path it replaces. `bench/foundations/p23-solid.mjs` times a whole tick on both
|
|
299
|
+
paths with one effect per entity. Once the path is warm, the plugin is the cheaper of the two.
|
|
300
|
+
That holds at every dense density the probe measures, and the gap widens with density. Its publish
|
|
301
|
+
alone, with no subscriber, costs less as well. On a tick that moves one row or no row the older
|
|
302
|
+
path measures lower.
|
|
303
|
+
|
|
304
|
+
What is untested. Under the test runner, `solid-js` resolves to its server build. There a signal
|
|
305
|
+
holds a value, consults no comparator and schedules no effect. The tests assert the value, and they
|
|
306
|
+
assert that `eq` reaches the signal and then run it by hand. The suite renders no component. It
|
|
307
|
+
proves nothing about a `<For>` re-render.
|
|
308
|
+
|
|
309
|
+
`src/plugins/solid/__tests__/solid.test.ts` locks the seed, the by-id publish, and the spawn and
|
|
310
|
+
the despawn. It locks the disable and the enable, the column grain, and one batch for each update.
|
|
311
|
+
It locks the cell identity across a delete, and the `eq` the cell carries. It locks coexistence
|
|
312
|
+
with observers, the singleton reset and the sparse refusal.
|
|
313
|
+
|
|
314
|
+
### Removed (breaking). `ECS_ERROR.ENTITY_NOT_DISABLED`
|
|
315
|
+
|
|
316
|
+
The code was declared in 0.4.0 and no path ever threw it. `enable` on a live entity is a no-op by
|
|
317
|
+
contract, so the fault it named cannot occur. A handler that compares against it no longer compiles.
|
|
318
|
+
Delete the branch.
|
|
319
|
+
|
|
320
|
+
### Removed (breaking). The signals kernel, its ECS bridge, and the kernel-to-Solid adapter
|
|
321
|
+
|
|
322
|
+
`@oasys/oecs/reactive` and `@oasys/oecs/reactive-sync` are gone. So are the adapter functions on
|
|
323
|
+
`@oasys/oecs/solid`: `fromKernel`, `fromKernelMap`, `fromKernelStruct` and `fromKernelArray`.
|
|
324
|
+
`@oasys/oecs/solid` now exports the `solid()` plugin alone.
|
|
325
|
+
|
|
326
|
+
The `solid()` plugin is the one path from ECS state into a UI. It reads the change feed and writes
|
|
327
|
+
Solid. The kernel and its mirror were a second path to the same place. They put two more graphs in
|
|
328
|
+
between. `component`, `fields` and `singleton` replace `syncComponentToMap`, `syncFieldsToMap` and
|
|
329
|
+
the two singleton bridges. `batchedUpdate` has no replacement, because a view publishes inside one
|
|
330
|
+
Solid `batch` at the settle point of `update()`.
|
|
331
|
+
|
|
332
|
+
There is **no React path in this release**, and no framework-free reactive path. A consumer that is
|
|
333
|
+
not a Solid app polls the world. Take `ecs.getField`, a cursor, or a `changed()` query.
|
|
334
|
+
`syncJoinToMap` also has no replacement. A view subscribes to one component, so take one view for
|
|
335
|
+
each component and combine them where you read.
|
|
336
|
+
|
|
337
|
+
### Changed. The host write seam drains at a phase the caller added
|
|
338
|
+
|
|
339
|
+
`HostCommandSeamOptions.schedules` took `readonly SCHEDULE[]`, so a seam could drain at one of the
|
|
340
|
+
seven built-ins only. It now takes `readonly SchedulePhase[]`, the same union `addSystems` takes, so
|
|
341
|
+
a handle from `ecs.addPhase` works. A plugin drains at the slot it owns. It no longer contends for
|
|
342
|
+
insertion order inside a phase the application also writes to. Every existing call still compiles,
|
|
343
|
+
because a `SCHEDULE` member is a `SchedulePhase`.
|
|
344
|
+
|
|
345
|
+
The recorder's refusal now reads the loop of a phase and not its name. A phase added with
|
|
346
|
+
`loop: "fixed"` is refused the same way `SCHEDULE.FIXED_UPDATE` is, with
|
|
347
|
+
`ECS_ERROR.INVALID_RECORDER_SCHEDULE`, and the message names the phase. The reason is unchanged. A
|
|
348
|
+
fixed-loop drain sees the fixed timestep, and not the host update dt. So the replay would diverge.
|
|
349
|
+
The seed-time bucket follows the loop too, so a phase of the startup loop records as startup.
|
|
350
|
+
|
|
351
|
+
### Changed. A malformed event id and a malformed system id throw `ECSError`
|
|
352
|
+
|
|
353
|
+
The two id minters refused a value that is not an integer >= 0 with an `AssertionError`. They now
|
|
354
|
+
throw an `ECSError` carrying the new `ECS_ERROR.INVALID_EVENT_ID` and
|
|
355
|
+
`ECS_ERROR.INVALID_SYSTEM_ID`. Both checks are development only, and the production build is
|
|
356
|
+
unchanged. Neither minter is on a public entry, so only a `catch` that tested the assertion class
|
|
357
|
+
sees the change.
|
|
358
|
+
|
|
359
|
+
The reason is the bundle. `AssertionError` extends `AppError`. So a plugin bundle that reached it
|
|
360
|
+
carried a second error class, and bound the base from the package. `ECSError` already resolves to
|
|
361
|
+
the package root. The assertion class and its base now leave every plugin bundle, and
|
|
362
|
+
`dist_artifact.test.ts` locks that.
|
|
363
|
+
|
|
364
|
+
### Removed. `AppError` leaves `@oasys/oecs/internal`
|
|
365
|
+
|
|
366
|
+
The base class was on the tooling entry because a development plugin bundle bound it there. No
|
|
367
|
+
bundle reaches it now, so the entry drops it. `ECSError` and `isEcsError` are on the package root,
|
|
368
|
+
and they are what a consumer catches.
|
|
369
|
+
|
|
370
|
+
### Added. The store can start anywhere in its memory
|
|
371
|
+
|
|
372
|
+
`memory.storeBase` places the store header at a caller-chosen byte offset. Every offset the store
|
|
373
|
+
writes is now relative to that base. That covers the header, the column descriptors, the region
|
|
374
|
+
table and the rings. `capacity` is the span from the base. The store writes nothing below the base.
|
|
375
|
+
A wasm-backed world defaults to one page and refuses zero. A compiled module owns the low
|
|
376
|
+
addresses of its own linear memory. A safe Zig or Rust build cannot read address 0. A caller
|
|
377
|
+
places the base above the module's `__heap_base` and its run-time heap. `memoryPlan.storeBase`
|
|
378
|
+
reports the value. `WASM_STORE_BASE_BYTES` is exported from `@oasys/oecs/internal`.
|
|
379
|
+
|
|
380
|
+
`storeBaseAbove(exports, extraBytes)` reads a module's `__heap_base` export, adds the run-time heap
|
|
381
|
+
the caller reserves, and rounds up to a whole page. So the base clears everything the module owns.
|
|
382
|
+
|
|
383
|
+
A checked-in WebAssembly module, built with no toolchain, now reads a live store in the test suite.
|
|
384
|
+
It agrees with the TypeScript side on the layout walk and the byte digest. It also agrees on an f32
|
|
385
|
+
kernel and the deterministic state hash. The layout is a tested ABI, not a fixture that TypeScript
|
|
386
|
+
compares with itself.
|
|
387
|
+
|
|
388
|
+
### Added. One system across workers
|
|
389
|
+
|
|
390
|
+
`workers()` from `@oasys/oecs/workers` is a plugin. `ECS.create({ plugins: [workers()] })` gives a
|
|
391
|
+
world `ecs.workers`, which carries `attach(options)`, `pool` and `detach()`. The pool, the plan
|
|
392
|
+
builder and the shim that reaches the node threads module ship in that subpath. So a world that
|
|
393
|
+
never names it carries none of them. A JavaScript caller reading `world.workers.attach` on a bare
|
|
394
|
+
world gets `ECS_ERROR.PLUGIN_NOT_INSTALLED`, and the message names the import. `AttachWorkersOptions`,
|
|
395
|
+
`WorkerPool`, `WorkersPlugin` and `DEFAULT_JOIN_TIMEOUT_MS` are exported from the same subpath.
|
|
396
|
+
`ParallelConfig`, `ParallelKernel` and `ParallelColumn` stay on the root, because they erase.
|
|
397
|
+
|
|
398
|
+
`world.workers.attach({ count })` starts a persistent pool on the package's own worker entry,
|
|
399
|
+
`@oasys/oecs/worker`. A system that carries a `parallel` config names a kernel a worker can load.
|
|
400
|
+
That kernel is a compiled `WebAssembly.Module` export, or an export of a JavaScript module URL. The
|
|
401
|
+
config also names the columns the kernel receives, in order. The schedule hands the pass to the
|
|
402
|
+
pool inside the same access span a TypeScript body gets. It parks the host on `Atomics.wait`, and
|
|
403
|
+
joins before the phase flush. No spawn, no despawn and no grow can overlap the workers. Nothing
|
|
404
|
+
else runs on the main thread while it is parked. Every worker computes its own row range per
|
|
405
|
+
archetype. It reads the descriptor row counts, its index and the worker count. So no plan crosses
|
|
406
|
+
the wire, and the result is deterministic. The join stamps every matched archetype for each
|
|
407
|
+
declared write.
|
|
408
|
+
|
|
409
|
+
A parallel system declares only `reads`, `writes` and a dense query. Sparse, relation, resource,
|
|
410
|
+
spawn, despawn and transition declarations, `exclusive`, and `backendHandle` are refused at
|
|
411
|
+
registration with `ECS_ERROR.PARALLEL_ACCESS`. Those refusals ship with the plugin. So a world that
|
|
412
|
+
installed no workers plugin validates no `parallel` config, builds no plan and runs the system's
|
|
413
|
+
`fn`. Below `parallel.minRows`, and without an attached pool, the system runs its `fn`. A heap
|
|
414
|
+
world cannot attach workers. A WASM kernel needs the wasm backing, because a `SharedArrayBuffer`
|
|
415
|
+
cannot be imported as a module memory.
|
|
416
|
+
|
|
417
|
+
The split pays only above a row count that depends on the machine, the kernel and the worker count.
|
|
418
|
+
`parallel.minRows` carries a measured default that sits above every crossover the probes found. The
|
|
419
|
+
probes cover both bodies, both kernel forms, both backings and every runtime tested. A world that
|
|
420
|
+
never tunes it never pays a pooled frame the sequential frame would have won. It gives up the gain
|
|
421
|
+
instead. A compute-bound kernel crosses far earlier and should set its own value. A caller's value
|
|
422
|
+
always wins. `bench/` holds the measurements and the tuning method.
|
|
423
|
+
|
|
424
|
+
With a bundler, pass `workerUrl` from the bundler's own URL import of the `@oasys/oecs/worker` entry, for
|
|
425
|
+
Vite `import workerUrl from "@oasys/oecs/worker?worker&url"`. The default resolution finds the entry beside
|
|
426
|
+
the package as it ships and not inside a bundle. A worker whose script does not load now fails
|
|
427
|
+
`workers.attach` with `ECS_ERROR.WORKERS_ENTRY_UNREACHABLE`. It terminates the pool, instead of
|
|
428
|
+
resolving with workers that never answer. The pool reaches the node threads module through
|
|
429
|
+
`process.getBuiltinModule`. So a browser build sees no node builtin specifier, and prints no warning.
|
|
430
|
+
|
|
431
|
+
`workers.attach` takes `joinTimeoutMs`, a safety net and not a budget. A worker that dies inside a pass
|
|
432
|
+
can never report done, and the parked host would wait forever. On timeout the frame throws
|
|
433
|
+
`PARALLEL_KERNEL_FAILED`. The pool enters a failed state in which every later frame runs `fn`.
|
|
434
|
+
`detach` terminates the hung worker.
|
|
435
|
+
|
|
436
|
+
At the join every worker adds one to a done word. The worker whose add completes the count
|
|
437
|
+
wakes the host. So the host wakes once for a pass, whatever the worker count is. `bench/` holds
|
|
438
|
+
the measurement beside a per-worker done word and a tree join, both of which cost more.
|
|
439
|
+
|
|
440
|
+
### Added. A kernel module contract that holds for any toolchain
|
|
441
|
+
|
|
442
|
+
Every worker of the pool instantiates one module over one memory. `docs/api/parallel.md` now states
|
|
443
|
+
what that costs a module, and what a build has to do about it. The list is the one import, the
|
|
444
|
+
export and its arity, and the store base. It also covers the stack, the data segment and the heap.
|
|
445
|
+
It carries one build line for Zig, for Rust, for C through `zig cc` and for AssemblyScript.
|
|
446
|
+
|
|
447
|
+
`registerSystem` refuses a `wasm` kernel module the pool cannot serve, with the new
|
|
448
|
+
`ECS_ERROR.PARALLEL_KERNEL_MODULE`. An import other than `env.memory` is named in the message. A
|
|
449
|
+
module that imports no memory is refused as well. It addresses a linear memory of its own,
|
|
450
|
+
writes rows nothing reads, and reports success. An export name the module does not carry, and an
|
|
451
|
+
export that is not a function, are the other two. Development builds only, at registration.
|
|
452
|
+
|
|
453
|
+
A worker now checks the export's parameter count against the column count plus three. It fails the
|
|
454
|
+
kernel load with both numbers when they disagree.
|
|
455
|
+
|
|
456
|
+
Five modules are checked into the test suite. The four toolchains above built them, and so did a
|
|
457
|
+
hand-written emitter that uses no toolchain. Each carries the same bodies. Each runs on the real
|
|
458
|
+
pool across several workers. Each must leave the bytes the sequential TypeScript body leaves. Five
|
|
459
|
+
more carry one fault each, so every refusal above has a real module behind it. The suite proves the
|
|
460
|
+
contract on a machine with no compiler installed.
|
|
461
|
+
|
|
462
|
+
### Added. Each worker instance owns its shadow stack, and `stackBytes` sizes it
|
|
463
|
+
|
|
464
|
+
A worker gives each instance of a `wasm` kernel module its own shadow stack.
|
|
465
|
+
|
|
466
|
+
An LLVM build, which is Zig, Rust, C and others, keeps a shadow stack in linear memory. It addresses
|
|
467
|
+
that stack through the mutable global `__stack_pointer`. A wasm global is per-instance, and every
|
|
468
|
+
instance starts at the address the linker chose. So every worker wrote its frames to the same bytes.
|
|
469
|
+
A kernel could spill a local array, a struct passed by pointer, or the address of a local. It then
|
|
470
|
+
read back what another worker wrote. The corruption was silent, and it needed no shared column. No
|
|
471
|
+
probe before this one caught it. The earlier kernels held every value in a wasm local.
|
|
472
|
+
|
|
473
|
+
The worker now carves one region for each worker out of `[__heap_base, storeBase)`. It moves
|
|
474
|
+
`__stack_pointer` to the top of its own. The regions come off the top of that span, downward from
|
|
475
|
+
the store base. So worker `i` gets its top at `storeBase - i * stackBytes`.
|
|
476
|
+
|
|
477
|
+
`workers.attach({ stackBytes })` says how big one region is, and everything below the lowest region
|
|
478
|
+
stays the module's heap. Reserve the module's peak run-time heap plus one stack for each worker with
|
|
479
|
+
`storeBaseAbove`. Then pass the same `stackBytes` to the pool. Omit it and the pool divides the whole
|
|
480
|
+
span, which leaves the module no heap. That is the default, and it suits a kernel that allocates
|
|
481
|
+
nothing. The heap rule asks for that anyway.
|
|
482
|
+
|
|
483
|
+
`stackBytes` must be an integer, a multiple of the frame alignment of 16, and at least one WASM page.
|
|
484
|
+
A value outside that fails the attach with `WORKERS_COUNT_INVALID`. A span too small to hold one
|
|
485
|
+
region for each worker fails the kernel load with `PARALLEL_KERNEL_FAILED`. The message names the
|
|
486
|
+
span, the region, the worker count and the remedy. A module that exports no `__stack_pointer` is left
|
|
487
|
+
alone. The docs say such a kernel may not use a stack. One worker needs no region, because one
|
|
488
|
+
instance owns the linked stack alone.
|
|
489
|
+
|
|
490
|
+
The assignment runs once for each kernel load, so a pass pays nothing for it.
|
|
491
|
+
|
|
492
|
+
### Changed (breaking for a module that reads the layout). `SIM_ABI_VERSION` is 1
|
|
493
|
+
|
|
494
|
+
A reader that carries version 0 measured every offset from buffer byte 0. A module that treated a
|
|
495
|
+
`byte_off` as a buffer address must add the store base it receives through `setLayout`. Restore and
|
|
496
|
+
resume accept a version 0 snapshot. Every version 0 store sat at byte 0, and its offsets read
|
|
497
|
+
correctly as offsets from the header. So a snapshot the 0.5 line wrote still restores. Any other
|
|
498
|
+
version is refused.
|
|
499
|
+
|
|
500
|
+
The archetype descriptor header grows from 36 bytes to 40 and gains `entity_ids_off` at offset 36.
|
|
501
|
+
The field is reserved for the archetype's row-to-entity table. The store writes zero, which says
|
|
502
|
+
the archetype carries no such table. A walker steps to the next record by `40 + column_count * 16`.
|
|
503
|
+
A reader that ignores the field reads every other field as before. A snapshot carries the
|
|
504
|
+
descriptor bytes. So restore rewrites a version 0 region at the new width, before it reads anything
|
|
505
|
+
else. The world's `stateHash` is unchanged, because it never folds a descriptor.
|
|
506
|
+
|
|
507
|
+
### Changed. `ComputeBackend.run` takes `dt` and the tick
|
|
508
|
+
|
|
509
|
+
`ComputeBackend.run(handle, deltaTime, tick)` replaces `run(handle)`. A backend that still declares
|
|
510
|
+
`run(handle)` keeps compiling and keeps running, because the extra arguments are ignored. Only code
|
|
511
|
+
that calls `run` itself sees the new shape. A module body needs `dt`, and
|
|
512
|
+
neither `dt` nor the frame tick lives in the bytes. The schedule also publishes the descriptor row
|
|
513
|
+
counts before every backend dispatch. So a module never reads a stale count. That covers a host
|
|
514
|
+
spawn before `startup()`, and a spawn from a run condition.
|
|
515
|
+
|
|
516
|
+
A caller-supplied `WebAssembly.Memory` may now carry `maxBytes`. The store needs a cap to promise
|
|
517
|
+
its span, so the cap is `maxBytes` or the default ceiling.
|
|
518
|
+
|
|
519
|
+
### Fixed
|
|
520
|
+
|
|
521
|
+
A kernel that would not load rejected `workers.attach` and left its workers running. So a node
|
|
522
|
+
process never exited on its own. The pool now ends the workers before the fault leaves.
|
|
523
|
+
|
|
524
|
+
An `ECSError` built on an engine without `Error.captureStackTrace` was a `TypeError` with no
|
|
525
|
+
category. The base class now checks for that V8 extension before it calls it. `error.name` read as
|
|
526
|
+
one minified letter in the production build, because it came off the constructor. It is now the
|
|
527
|
+
literal `ECSError`.
|
|
528
|
+
|
|
529
|
+
The command, event and action rings copied slot payloads through a view built from buffer byte 0.
|
|
530
|
+
At a nonzero base they wrote below the store. The rings now add the view offset.
|
|
531
|
+
|
|
532
|
+
`wasmMemoryAllocator` predicted the JavaScriptCore write cost of a growable `SharedArrayBuffer`. A
|
|
533
|
+
shared `WebAssembly.Memory` does not pay it. The comment now says so, and `bench/` holds the
|
|
534
|
+
measurement.
|
|
535
|
+
|
|
536
|
+
### Changed. The store's observation seam takes a consumer name
|
|
537
|
+
|
|
538
|
+
`Store.configureObservation` and `Store.configureSparseObservation` take the consumer name first, and
|
|
539
|
+
the store merges every consumer's record instead of holding one. The structural hook seam is now a
|
|
540
|
+
list that every consumer joins. It was one dispatch bound to the observer registry. The registry is
|
|
541
|
+
now one consumer among others, named `observers`.
|
|
542
|
+
|
|
543
|
+
The observers plugin behaves as it did. It still owns the access-topological order, the radix
|
|
544
|
+
entity order, the access span of each callback and `yieldExisting`.
|
|
545
|
+
`src/core/ecs/__tests__/unit/change_feed.test.ts` locks the flag merge, the drain memo, and the
|
|
546
|
+
install order of both hook lists.
|
|
547
|
+
|
|
548
|
+
### Changed (breaking). The store no longer forwards to its collaborators
|
|
549
|
+
|
|
550
|
+
Thirty methods on the internal `Store` forwarded one operation each to a collaborator, and carried
|
|
551
|
+
no logic. A caller now names the owner: `store.relations.addRelation`, `store.events.emit`,
|
|
552
|
+
`store.resources.get`, `store.snapshots.snapshot`. The forwarding hid which object held the state and
|
|
553
|
+
widened the class for nothing.
|
|
554
|
+
|
|
555
|
+
### Changed. The query terms travel as one record
|
|
556
|
+
|
|
557
|
+
A query carries two kinds of term. A dense term sets a bit in the component mask and picks the
|
|
558
|
+
archetypes. Every other term now rides in one `QueryTerms` record. Those terms are sparse
|
|
559
|
+
membership, optional fetch, include-disabled, the `(R, *)` wildcard, hierarchy ordering, and an
|
|
560
|
+
archetype term from `where`. `Query`'s constructor takes one parameter where it took seven. The
|
|
561
|
+
three driver seams take one where they repeated four. A query that declares no such term shares
|
|
562
|
+
one frozen record. Adding a term is one edit instead of five.
|
|
563
|
+
|
|
564
|
+
### Changed. `Commands` and `SystemContext` moved to their own module
|
|
565
|
+
|
|
566
|
+
`query.ts` held the read side and the write side of the system-facing interface. The write side is
|
|
567
|
+
`system_context.ts` now. Every export is unchanged, and the barrel re-exports both.
|
|
568
|
+
|
|
569
|
+
### Fixed. The build no longer splits the core entry into small chunks
|
|
570
|
+
|
|
571
|
+
Declaring the plugin entries beside the core entries put them in one rollup graph. Rollup
|
|
572
|
+
then split `index.js` into ten small shared chunks. Those splits are real module boundaries at run
|
|
573
|
+
time. A measurement of `spawn` against the shipped artifact showed the cost. The plugins
|
|
574
|
+
build in their own pass now, and the core chunk graph is unchanged.
|
|
575
|
+
|
|
576
|
+
### Changed (breaking). A name that misdescribed its body now says what it does
|
|
577
|
+
|
|
578
|
+
A name that promises one act and performs another sends a reader to the wrong conclusion without
|
|
579
|
+
opening the body. An audit read every named function and method in `src/` against its body. Each
|
|
580
|
+
name that failed is now the act it performs. The old names are removed and not aliased, because a
|
|
581
|
+
name that stays reachable keeps teaching the wrong model.
|
|
582
|
+
|
|
583
|
+
The public surface:
|
|
584
|
+
|
|
585
|
+
| 0.5 | 0.6 |
|
|
586
|
+
| --- | --- |
|
|
587
|
+
| `query.eachChunk(cb)` | `query.forEachChunk(cb)` |
|
|
588
|
+
| `query.forEachUntil(cb)` | `query.some(cb)` |
|
|
589
|
+
| `ctx.read(key)` | `ctx.readEvents(key)` |
|
|
590
|
+
| `ecs.onStoreLayoutPublished(fn)` | `ecs.subscribeLayout(fn)` |
|
|
591
|
+
| `ecs.publishArchetypeRowCounts()` | `ecs.publishRowCounts()` |
|
|
592
|
+
| `queue.pending` | `queue.pendingCount` |
|
|
593
|
+
| `FrameTraceSink.systemStart` | `FrameTraceSink.systemBegin` |
|
|
594
|
+
| `column.get(i)`, on `/primitives` | `column.getAt(i)` |
|
|
595
|
+
| `column.ensureCapacity(n)`, on `/primitives` | `column.reserve(n)` |
|
|
596
|
+
|
|
597
|
+
`ctx.read` moves because `cols.read(def)` in the same walk returns a column group, so one verb
|
|
598
|
+
carried two shapes. `forEachUntil` returns whether a callback accepted, thus it is a predicate and
|
|
599
|
+
now reads as one. `reserve` is the contract of `ColumnBacking`: guarantee room for the count, or
|
|
600
|
+
throw. A heap column grows to keep it. A buffer-backed column cannot grow, so it throws, and each
|
|
601
|
+
doc states which.
|
|
602
|
+
|
|
603
|
+
On `@oasys/oecs/internal`, every `accessCheck.check*` method is now `assert*`, and
|
|
604
|
+
`dispatchTrace.recordEmit` and `recordRead` are `recordEventEmit` and `recordEventRead`.
|
|
605
|
+
|
|
606
|
+
Three rules now hold across the tree. One verb throws on a bad state, `assert`, and `validate`
|
|
607
|
+
keeps only the helpers that return the value they test. One verb constructs, `create`. The
|
|
608
|
+
underscore prefix marks a private or a protected class member and nothing else. So a member that
|
|
609
|
+
another module reaches carries no prefix.
|
|
610
|
+
|
|
611
|
+
### Changed (breaking). A field name now says what it holds
|
|
612
|
+
|
|
613
|
+
The same audit read every field and every module-scope variable. A field whose name promised one
|
|
614
|
+
content and held another is now the content it holds. The underscore prefix now marks a private
|
|
615
|
+
or a protected member on a field. It does so exactly as it does on a method. A member that another
|
|
616
|
+
module reaches carries no prefix, whatever its role.
|
|
617
|
+
|
|
618
|
+
The public surface:
|
|
619
|
+
|
|
620
|
+
| 0.5 | 0.6 |
|
|
621
|
+
| --- | --- |
|
|
622
|
+
| `query._defs` | `query.defs` |
|
|
623
|
+
| `query._include` | `query.include` |
|
|
624
|
+
| `query._id` | `query.id` |
|
|
625
|
+
| `query._sparseInclude`, `query._sparseExclude` | `query.terms.sparseIncludes`, `query.terms.sparseExcludes` |
|
|
626
|
+
| `query._optional` | `query.terms.optionalTerms` |
|
|
627
|
+
| `query._includeDisabled` | `query.includesDisabled` |
|
|
628
|
+
| `query._relationIncludes`, `query._relationExcludes` | `query.terms.relationIncludes`, `query.terms.relationExcludes` |
|
|
629
|
+
| `query._hierarchy` | `query.terms.hierarchyTerm` |
|
|
630
|
+
| `cols._arch`, `cols._tick`, on `ChunkColumns` | `cols.arch`, `cols.tick` |
|
|
631
|
+
| `ecs._caches` | `ecs.caches` |
|
|
632
|
+
| `bitset._words`, on `/primitives` | `bitset.words` |
|
|
633
|
+
|
|
634
|
+
None of these is part of the documented API. Each is public because another module reads it, so the
|
|
635
|
+
prefix claimed a privacy the member never had. Three of them could not drop the prefix alone.
|
|
636
|
+
`Query` already carries an `optional`, an `includeDisabled` and a `hierarchy` method. Each
|
|
637
|
+
of those three now names the thing it holds: a term list, a flag, a term. The terms that are not
|
|
638
|
+
dense then moved into the one `terms` record described above. `includesDisabled` stayed on the
|
|
639
|
+
query as its own copy.
|
|
640
|
+
|
|
641
|
+
Inside the package the same rule moved about a hundred more members. `Archetype` publishes
|
|
642
|
+
`flatColumns`, `bufs`, `accessorColumns`, `colOffset`, `fieldCount`, `columnIds` and `changedTick`
|
|
643
|
+
without a prefix. `Store` publishes `tick`, `trace`, `anyDirtyTracked` and `queryDirtyEpoch`.
|
|
644
|
+
Every private field of `Store`, `Schedule`, `ECS`, `AccessCheck`, `EventRegistry` and the editor now
|
|
645
|
+
carries one. A table that holds one entry per entity reads as plural, so `Store.entityRow` is
|
|
646
|
+
`_entityRows` and `Store.entityArchetype` is `_entityArchetypes`. `Schedule.setConditions` and
|
|
647
|
+
`setOrdering` read as verbs and are `_conditionsBySet` and `_orderingBySet`. `EventRegistry.count`
|
|
648
|
+
allocates the next event id and is `_nextEventId`. `DispatchTrace.buf` holds counts, not bytes, and
|
|
649
|
+
is `_counts`.
|
|
650
|
+
|
|
651
|
+
### Fixed. A fifth column type made every row move in the process slow
|
|
652
|
+
|
|
653
|
+
Every structural row operation walked the columns of an archetype through one loop. That loop had
|
|
654
|
+
one typed-array access site. The operations are the copy behind `addComponent` and
|
|
655
|
+
`removeComponent`, the swap-remove behind `despawn`, and the swaps behind `disable` and `enable`.
|
|
656
|
+
The site saw every column type that any archetype in the process used. V8 keeps one site fast for
|
|
657
|
+
at most four typed-array classes. At the fifth type the site became megamorphic. Each element move
|
|
658
|
+
then cost far more, in every archetype, and not only in the one that mixed the types. The library
|
|
659
|
+
offers eight column types. Consider a schema with `f32` positions, an `i32` counter, a `u8` flag, a
|
|
660
|
+
`u16` team and an `f64` timer. It reached the fifth type without notice.
|
|
661
|
+
|
|
662
|
+
The structural operations now move bits through views whose class depends on the element width
|
|
663
|
+
alone (`Uint8Array`, `Uint16Array`, `Uint32Array`, `Float64Array`). So the site sees at most four
|
|
664
|
+
classes. A write of a number must convert to the column's type. So the value-writing paths use the
|
|
665
|
+
true view, through one access site for each type. Those paths are `spawn` with a template, and
|
|
666
|
+
`addComponent` with values. The cost of a row move is now flat across the number of column types.
|
|
667
|
+
It is unchanged for a world that uses one type.
|
|
668
|
+
|
|
669
|
+
### Changed. A sparse component is id-indexed, and `sparseCursor` is the fastest read by id
|
|
670
|
+
|
|
671
|
+
A sparse component kept each entity's values in a small JavaScript array. A map keyed by entity
|
|
672
|
+
index held those arrays. So a read by id paid a lookup and a pointer chase. It also stored a value
|
|
673
|
+
as the number given, and not as the declared type. The store now keeps one typed array of the
|
|
674
|
+
declared type for each field, indexed by entity index. A sparse set sits beside them for
|
|
675
|
+
membership. A read by id is one load. An add or a remove is a bit and a write at the index. A value
|
|
676
|
+
converts as the field's type converts, as a dense field does. An `i32` truncates, a `u8` wraps, an
|
|
677
|
+
`f32` rounds.
|
|
678
|
+
|
|
679
|
+
`sparseCursor` and `sparseCursorRead`, on `ecs` and on `ctx`, are the sparse form of `cursor`.
|
|
680
|
+
`at` writes the entity index alone, and a field access is one load. So a sparse cursor is the read
|
|
681
|
+
by id to use. Take it when a system touches many entities from a list of ids. In development, `at`
|
|
682
|
+
throws for a dead entity or a non-member. In production it does not test. `ctx.sparseCursor` needs the
|
|
683
|
+
component in `sparseWrites`, and `ctx.sparseCursorRead` in `sparseReads`.
|
|
684
|
+
|
|
685
|
+
The columns of a store double to fit the highest member index. So the memory of a sparse component
|
|
686
|
+
is proportional to that index, and not to the member count. The snapshot format is unchanged. The
|
|
687
|
+
`indices` view of a store is now a typed view with a fixed length. The query driver walks the live
|
|
688
|
+
member list, so a walk sees an edit made during it as before. That walk is `forEachEntity` over a
|
|
689
|
+
sparse term. It now keeps the dense verdict of the last archetype it saw. So members of one
|
|
690
|
+
archetype that sit together in the sparse list pay the mask test one time, not each.
|
|
691
|
+
|
|
692
|
+
### Fixed. Every ref and cursor read paid for a property key that the optimizer could not fold
|
|
693
|
+
|
|
694
|
+
The state of a ref or cursor was keyed by two symbols that other modules imported. The package
|
|
695
|
+
build puts the accessor module and the store in different chunks. A key read through an import
|
|
696
|
+
binding is not a constant to the optimizer. So every field access through a ref or a cursor was a
|
|
697
|
+
generic keyed load. A symbol has no other way to be reached. So the state is now keyed by two
|
|
698
|
+
reserved names, `__cols` and `__row`. Each site writes them as literals. Registration refuses those
|
|
699
|
+
two names on a dense or a sparse component.
|
|
700
|
+
|
|
701
|
+
Two more constants on the by-id paths are read through local copies, for the same reason. They are
|
|
702
|
+
the entity index mask, and the entity-id bounds that `isAlive` compares against. The `switch`
|
|
703
|
+
over column types in the value-write path compares against local copies of the tags. So it compiles
|
|
704
|
+
to a jump table.
|
|
705
|
+
|
|
706
|
+
The shape of the accessors is also settled when the module loads. A dense cursor reassigns its
|
|
707
|
+
column array on every `at`. A ref or a sparse cursor never does. So the first dense `at` in a
|
|
708
|
+
process changed a field of the shared shape from constant to mutable. Every optimized function
|
|
709
|
+
that had read the field under the constant assumption was thrown away. It compiled again, slower.
|
|
710
|
+
One throwaway object of each shape now reassigns both fields at load. So nothing compiles under the
|
|
711
|
+
assumption, and nothing is thrown away.
|
|
712
|
+
|
|
713
|
+
### Fixed. Systems made from one factory ran their hot loops much slower
|
|
714
|
+
|
|
715
|
+
V8 decides what to inline from the feedback of a call site. Every system in a world could come from
|
|
716
|
+
one function literal. A factory such as `makeMover(component)` does that, and so does a world with
|
|
717
|
+
a single system. The scheduler's dispatch site then saw one target. The engine inlined the system
|
|
718
|
+
body into the scheduler's own loop over the systems. That body carried its `eachChunk` callback and
|
|
719
|
+
its hot loop. That inlined loop ran much slower than the same loop compiled on its own. A world
|
|
720
|
+
whose systems came from two or more literals never hit this. So the factory case was slower than
|
|
721
|
+
the plain case. Nothing in the user's code said why.
|
|
722
|
+
|
|
723
|
+
Every system body now runs through one trampoline whose call site the module makes megamorphic
|
|
724
|
+
when it loads. No system body is inlined into the scheduler, whatever the number of literals, and
|
|
725
|
+
each is compiled on its own. A dispatch costs a little more, one time for each system in each phase.
|
|
726
|
+
A system that does any work gains more than that cost.
|
|
727
|
+
|
|
728
|
+
### Changed. The cost of a new archetype no longer grows with the number of archetypes
|
|
729
|
+
|
|
730
|
+
A world discovers its archetypes as it runs, and each new one extends the column store. That extend
|
|
731
|
+
took the in-place path, which moves no rows. But it still walked every archetype three times. One
|
|
732
|
+
walk built a list of row counts that only the realloc path reads. One summed the descriptor bytes
|
|
733
|
+
in use. One copied the archetype map. So the N-th archetype cost N steps, and a world with many
|
|
734
|
+
archetypes paid for that at startup. The list is now built only when the realloc path runs. The
|
|
735
|
+
store caches the descriptor bytes in use. The in-place extend appends to the archetype
|
|
736
|
+
map, instead of copying it. The cost of a new archetype is now the cost of its own columns.
|
|
737
|
+
|
|
738
|
+
### Fixed. A fifth component made every ref and cursor in the process slow
|
|
739
|
+
|
|
740
|
+
A ref got one prototype for each (archetype, component) pair, and a cursor one for each component.
|
|
741
|
+
An engine gives an object a distinct shape for each distinct prototype. So the read of the row
|
|
742
|
+
inside each getter saw one shape for each component. That covers every component the program read
|
|
743
|
+
through refs or cursors. At the fifth shape that read became megamorphic. Every field access
|
|
744
|
+
through every ref and every cursor in the process became far slower. A world with five components,
|
|
745
|
+
each with only `f64` fields, was enough.
|
|
746
|
+
|
|
747
|
+
Every ref and every cursor now shares one prototype for the whole process. Each distinct field name
|
|
748
|
+
gets one global id and one accessor on the prototype. The first component registration that uses
|
|
749
|
+
the name installs them. An accessor holds the component's columns indexed by that id. So a field
|
|
750
|
+
read costs the same two index operations it did before. The own state of an accessor is the two
|
|
751
|
+
reserved names `__cols` and `__row`. Registration refuses both as field names, so no field can
|
|
752
|
+
collide with it. A field the component does not have throws `FIELD_NOT_REGISTERED` under `DEV`,
|
|
753
|
+
where it read a neighbouring column before.
|
|
754
|
+
|
|
755
|
+
`for..in` over a ref or a cursor is no longer a way to list a component's fields. Use the schema.
|
|
756
|
+
The shared prototype carries the field name of every component registered in the process. The
|
|
757
|
+
two reserved names are own fields. So the walk reports all of them. `Object.keys` and the spread
|
|
758
|
+
report the two reserved names alone, where they reported nothing before.
|
|
759
|
+
|
|
760
|
+
Two components may give one field name different types. An `x` may be `f32` in one and `i32` in
|
|
761
|
+
another. That name reads and writes correctly on both. Its accessor dispatches on the column's
|
|
762
|
+
class. That costs one `switch` more than an accessor for a name with one type.
|
|
763
|
+
|
|
764
|
+
### Changed (breaking). `memory` is two fields, and not one union of five arms
|
|
765
|
+
|
|
766
|
+
`ECSOptions.memory` held two questions inside one key-discriminated union. The questions do not
|
|
767
|
+
depend on each other: how big the world is, and what holds its bytes. A caller could answer only
|
|
768
|
+
one of them. The `budget` arm and the `maxBytes` arm each selected the heap allocator themselves.
|
|
769
|
+
So "a budget of 50,000 entities on a shared backing" was not something you could say. `maxBytes`
|
|
770
|
+
had to appear three times, once for each backing arm, because the size axis had nowhere else to
|
|
771
|
+
live.
|
|
772
|
+
|
|
773
|
+
Sizing and backing are now two fields, and every pair of them is legal:
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
new ECS({ memory: { entities: 50_000 } }); // size only
|
|
777
|
+
new ECS({ memory: { backing: "shared" } }); // storage only
|
|
778
|
+
new ECS({ memory: { entities: 50_000, backing: "shared" } }); // both, this was a type error before
|
|
779
|
+
new ECS({ memory: { entities: 50_000, maxBytes: 64 * MiB } });// size from one, ceiling from the other
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Each removed arm throws `INVALID_MEMORY_OPTIONS` and names its new spelling. They are removed and
|
|
783
|
+
not aliased. A sizing that the engine ignored in silence would build a world of the wrong
|
|
784
|
+
size. It would show that much later, as a limit error far from its cause.
|
|
785
|
+
|
|
786
|
+
| 0.5 | 0.6 |
|
|
787
|
+
| --- | --- |
|
|
788
|
+
| `{ budget: { entities: N } }` | `{ entities: N }` |
|
|
789
|
+
| `{ heap: { maxBytes: X } }` | `{ maxBytes: X, backing: "heap" }` |
|
|
790
|
+
| `{ shared: { maxBytes: X } }` | `{ maxBytes: X, backing: "shared" }` |
|
|
791
|
+
| `{ wasm: W }` | `{ backing: { wasm: W } }` |
|
|
792
|
+
| `{ allocator: A, capBytesHint: X }` | `{ maxBytes: X, backing: { allocator: A } }` |
|
|
793
|
+
|
|
794
|
+
`maxBytes` and `columnCapacity` keep their names and their meaning. The types `EntityBudget` and
|
|
795
|
+
`SharedMemoryArm` are removed. The three fields of a budget are now fields of `memory` itself. The
|
|
796
|
+
shared backing is the string `"shared"`. The new type `MemoryBacking` names the backing axis.
|
|
797
|
+
`ResolvedECSMemory.source` now names the backing alone, and the new field `sizing` names the size
|
|
798
|
+
axis.
|
|
799
|
+
|
|
800
|
+
### Fixed. A custom allocator with a limit below about 12.6 MiB could not build a world
|
|
801
|
+
|
|
802
|
+
The `allocator` arm reserved the full identity space for the entity index, always, and ignored the
|
|
803
|
+
limit that `capBytesHint` declared. The reservation happens when the store is built. So the index
|
|
804
|
+
alone did not fit under a small limit. The world threw `STORE_CAP_EXCEEDED` before it existed.
|
|
805
|
+
The error then blamed the caller for runaway entity growth, in a world that held no entities. Every
|
|
806
|
+
other arm already sized the index from the limit.
|
|
807
|
+
|
|
808
|
+
The reservation of the entity index now comes from `entities` first, then the byte limit, then the
|
|
809
|
+
default. That holds for each backing equally.
|
|
810
|
+
|
|
811
|
+
### Fixed. A declared number of entities now sizes the entity index on every backing
|
|
812
|
+
|
|
813
|
+
Only the `budget` arm derived the index from the entity count, and that arm forced the heap backing.
|
|
814
|
+
On each other backing the index was sized backwards from the byte limit. That reserves much more
|
|
815
|
+
than a small world needs. A count now reaches the index whichever backing holds the bytes.
|
|
816
|
+
|
|
817
|
+
### Added. `fixedSabAllocator`, a shared buffer that does not grow
|
|
818
|
+
|
|
819
|
+
`fixedSabAllocator(maxBytes)`, from `@oasys/oecs/shared`, reserves one fixed `SharedArrayBuffer` at
|
|
820
|
+
the limit. It is `heapArrayBufferAllocator` with a `SharedArrayBuffer`. So the bytes stay shareable
|
|
821
|
+
with a worker or a WASM module, and the buffer never moves.
|
|
822
|
+
|
|
823
|
+
It exists for a measured reason. JavaScriptCore has no fast store path for a TypedArray view over a
|
|
824
|
+
growable `SharedArrayBuffer`. A column read costs what a fixed buffer costs. Every column write
|
|
825
|
+
costs far more. The cost is for each access and not for each byte, so a small world pays it too.
|
|
826
|
+
V8 shows no such difference. Safari and Bun are JavaScriptCore. A fixed buffer restores the fast
|
|
827
|
+
store path on both engine families and gives up only the growth.
|
|
828
|
+
|
|
829
|
+
`growableSabAllocator` and `wasmMemoryAllocator` now carry that warning in their own documentation.
|
|
830
|
+
A shared `WebAssembly.Memory` gives a growable `SharedArrayBuffer` and can give nothing else. So the
|
|
831
|
+
WASM backing should pay the same cost on JavaScriptCore. That last point is reasoning and not
|
|
832
|
+
measurement, and it is marked as such.
|
|
833
|
+
|
|
834
|
+
The default backing for `{ backing: "shared" }` is unchanged: it is still `growableSabAllocator`.
|
|
835
|
+
|
|
836
|
+
### Fixed. The fixed-length rule for a column view is now stated and locked
|
|
837
|
+
|
|
838
|
+
Each column view is built with an explicit `(byteOffset, length)`. A TypedArray built with no length
|
|
839
|
+
argument tracks the length of its buffer. Measurement puts that shape far behind a fixed-length
|
|
840
|
+
view on every engine tested. `createView` is the only place that builds a column view, but nothing
|
|
841
|
+
said so and nothing tested a view's length. The rule is now in the `createView` documentation, and
|
|
842
|
+
two tests hold it. One walks every column of every archetype. One proves that a view keeps its
|
|
843
|
+
length when the buffer below it grows. The second matters most, because a length-tracking view
|
|
844
|
+
survives the identity and data checks that were already there.
|
|
845
|
+
|
|
846
|
+
### Added. `ECS_ERROR.INVALID_TEMPLATE`
|
|
847
|
+
|
|
848
|
+
`spawn` and `spawnMany` take a template from `ecs.template(...)`. A component definition, a callable
|
|
849
|
+
bundle, the pre-0.5 array of entries, or some other value reached the store instead. The store then
|
|
850
|
+
failed with a `TypeError` about an internal field. That error named the wrong place, and it did not
|
|
851
|
+
say what to do. A development build now throws `INVALID_TEMPLATE`. It names the value the caller
|
|
852
|
+
gave, and it names the call to make in its place. `ecs.template` rejects the array of entries with
|
|
853
|
+
the same code. The types already reject all four shapes, so this catches an untyped call site. Both
|
|
854
|
+
checks are development only, and the production build is unchanged.
|
|
855
|
+
|
|
856
|
+
### Added. `ECS_ERROR.SNAPSHOT_RESTORE_FAILED`, and `ECSRestoreError` is an `ECSError`
|
|
857
|
+
|
|
858
|
+
`ecs.snapshots.restore` threw an `ECSRestoreError` that extended `Error` and carried no code. So a
|
|
859
|
+
caller who sorts faults by `category` saw a restore failure fall through. `ECSRestoreError` now
|
|
860
|
+
extends `ECSError` with the category `SNAPSHOT_RESTORE_FAILED`, and `isEcsError` answers true for
|
|
861
|
+
it. The class, its name and the message are unchanged, so a `catch` by class or by `err.name`
|
|
862
|
+
still holds. `StoreRestoreError` and `SparseRestoreError` stay plain `Error` classes. The
|
|
863
|
+
module that declares them imports nothing, and `core/store` does not see the code enum.
|
|
864
|
+
|
|
865
|
+
### Added. `InPlaceBufferAllocator` and `BufferAllocator` are type exports
|
|
866
|
+
|
|
867
|
+
`memory: { backing: { allocator } }` takes an `InPlaceBufferAllocator`. The type was declared in
|
|
868
|
+
`core/store` without a published entry that re-exports it. So a caller could not name the interface
|
|
869
|
+
a custom allocator implements. The root and `@oasys/oecs/shared` now export both types.
|
|
870
|
+
|
|
871
|
+
### Fixed. A write was reported on two frames when the writer ran before the reader
|
|
872
|
+
|
|
873
|
+
`changed()` compared a per-frame tick with the last run of the reader. It took a stamp at or
|
|
874
|
+
after that run. A frame tick cannot order a writer and a reader inside one frame. So a write by an
|
|
875
|
+
earlier system was reported on that frame, and again on the next. Writer before reader is the usual
|
|
876
|
+
order, so the usual order reported every write twice. The same tick missed a host write between
|
|
877
|
+
frames at an archetype-level `onSet`. The observer's baseline was the next frame, which a host
|
|
878
|
+
write never reached.
|
|
879
|
+
|
|
880
|
+
The engine now keeps a change tick apart from the frame tick. It advances before each system run,
|
|
881
|
+
and before each phase flush. It also advances before the `onSet` dispatch, and at the end of each
|
|
882
|
+
update. A write stamps it, and a consumer reports a stamp above its own last run. One write is
|
|
883
|
+
reported one time at each grain, whichever system runs first. A host write between frames reaches
|
|
884
|
+
both grains on the next update. `ctx.ecsTick` still counts frames. A system no longer sees its own
|
|
885
|
+
stamp on its next run. A writer that also read `changed()` on the same component fired on every
|
|
886
|
+
frame. It now fires for the writes of other systems. It also fires for its own writes, inside the
|
|
887
|
+
run that made them.
|
|
888
|
+
|
|
889
|
+
`ecs.getCurrentTick()` is `ecs.getChangeTick()`, and the schedule's `runStartup`, `runUpdate` and
|
|
890
|
+
`runFixedUpdate` no longer take a tick.
|
|
891
|
+
|
|
892
|
+
### Fixed. A `ref` or `cursor` write never reached an entity-level `onSet`
|
|
893
|
+
|
|
894
|
+
Only `setField`, `updateField` and `markChanged` recorded an entity for an `onSet` observer with
|
|
895
|
+
entity granularity. The change detection page said a `ref` write was seen. The accessor
|
|
896
|
+
setters write raw columns and cannot record. So `ctx.ref` records the entity when you create the
|
|
897
|
+
ref. A mutable cursor records it on each `at`, on the context and on the host. Both are
|
|
898
|
+
conservative, as the archetype stamp is. `refRead` and `cursorRead` record nothing. The entity
|
|
899
|
+
level drain also no longer allocates a list on each frame.
|
|
900
|
+
|
|
901
|
+
### Added. `cols.ticks(def)`, the row record for an entity-level `onSet`
|
|
902
|
+
|
|
903
|
+
A raw column write in a chunk loop is invisible to the engine. So an `onSet` observer with entity
|
|
904
|
+
granularity needed `ctx.markChanged` for each row. That is a call and a list push. The loop can now
|
|
905
|
+
store the change tick into the row of `cols.ticks(def)`: `t[i] = cols.tick`. One typed-array store,
|
|
906
|
+
which costs about what the write beside it costs. The column exists only while an entity-level
|
|
907
|
+
`onSet` observer tracks the component, and the call throws `ROW_TICKS_NOT_TRACKED` otherwise.
|
|
908
|
+
|
|
909
|
+
### Added. `ecs.trackRows(def)`, `cols.ticksRead(def)`, `cols.since` and `changed(def).forEachChunk`
|
|
910
|
+
|
|
911
|
+
Change detection at the row grain as a pull. `ecs.trackRows(def)` keeps one change tick for each
|
|
912
|
+
row of every archetype that holds `def`, stamped by every write path. A `ChangedQuery` now has
|
|
913
|
+
`forEachChunk`. Inside it, `cols.ticksRead(def)` is the row tick column. `cols.since` is the
|
|
914
|
+
change tick of the previous run of the system. So `t[i] > cols.since` picks the rows that changed
|
|
915
|
+
since that run. An `onSet` observer with entity granularity turns the row ticks on as well.
|
|
916
|
+
|
|
917
|
+
### Added. Change detection for a sparse component
|
|
918
|
+
|
|
919
|
+
A sparse component had none. It now has the row grain. `ecs.trackRows(def)` keeps one change tick
|
|
920
|
+
for each entity index in the sparse store. `setSparseField` and `at` on the mutable sparse cursor
|
|
921
|
+
stamp it. An add zeroes it. `ctx.sparseChanged(def, entityId)` reads it as a pull, true for the
|
|
922
|
+
run after a write. `observe(def, { granularity: "entity", onSet })` reads it as a push. That is the
|
|
923
|
+
one observer shape a sparse component takes. It has no archetype, so no structural callback and no
|
|
924
|
+
archetype grain. `observe` names that in its error. A mutable sparse cursor's `at` pays one load
|
|
925
|
+
and one branch while the component keeps no row ticks.
|
|
926
|
+
|
|
927
|
+
### Changed. A by-id record stops listing once the list outgrows a fraction of the live entities
|
|
928
|
+
|
|
929
|
+
An entity-level `onSet` drain paid a push, three checks and a sort slot for each recorded entity.
|
|
930
|
+
So a system that wrote most rows by id paid more than a scan of the rows would cost. Past a cap
|
|
931
|
+
set from the live entity count at each drain, a frame switches to the scan. The by-id record then
|
|
932
|
+
stamps the row and pushes nothing. The drain walks the plane of each archetype a writer stamped. A
|
|
933
|
+
`markChanged` record stamps no archetype, so it is listed still, and dropped when a scan covers it.
|
|
934
|
+
|
|
935
|
+
### Fixed. The observer drain took a slow path on JavaScriptCore
|
|
936
|
+
|
|
937
|
+
The radix pass that orders a drain by entity index kept its scratch in a plain array. A length
|
|
938
|
+
assignment grew that array. JavaScriptCore turns such an array into a sparse store, and each
|
|
939
|
+
element store in the pass became a hash insert. The scratch is a typed array now, grown by
|
|
940
|
+
doubling. That holds in the observer registry, and in the hierarchy walk of the relation service.
|
|
941
|
+
V8 did not care either way.
|
|
942
|
+
|
|
943
|
+
### Changed. The entity grain keeps a row tick, and not a dedup byte for each entity slot
|
|
944
|
+
|
|
945
|
+
An entity-level `onSet` observer used to allocate one byte for each entity index slot of the world.
|
|
946
|
+
It did that for each tracked component, whatever the live count. It now gives every archetype of
|
|
947
|
+
the component one word of ticks for each row. That word rides the row plane through every move.
|
|
948
|
+
The dirty list takes an entity one time per drain, by comparing that tick. A row carries its tick
|
|
949
|
+
across a transition, so the compare holds for a move. It does not hold when the entity leaves the
|
|
950
|
+
component and joins it again. The new row has no tick to carry. So the drain drops a repeated id
|
|
951
|
+
after it orders the list. The drain fires a row a chunk loop recorded, with no liveness check. A
|
|
952
|
+
row inside the enabled partition is alive, a member and enabled by construction. A row a
|
|
953
|
+
by-id path recorded is still checked. In a frame where a chunk loop took `cols.ticks(def)`, the
|
|
954
|
+
drain walks more rows. It walks each row of every archetype of the component that a writer stamped.
|
|
955
|
+
So take the column only in a loop that stores into it.
|
|
956
|
+
|
|
957
|
+
A tag keeps no row ticks. It has no field, so no write can record one, and `trackRows` on a tag
|
|
958
|
+
does nothing.
|
|
959
|
+
|
|
960
|
+
### Fixed. `observe()` on a sparse component named "component undefined"
|
|
961
|
+
|
|
962
|
+
A sparse component and a relation have no observers and no change tick. `observe` now throws
|
|
963
|
+
`OBSERVER_INVALID_CONFIG` and names the remedy.
|
|
964
|
+
|
|
965
|
+
## [0.5.4] - 2026-07-31
|
|
966
|
+
|
|
967
|
+
### Added. `cursor` and `cursorRead`, the accessor for a sweep by id
|
|
11
968
|
|
|
12
969
|
`ecs.cursor(def)` and `ecs.cursorRead(def)`, with the two equivalents on `ctx`, give a single-entity
|
|
13
|
-
accessor that you can **
|
|
970
|
+
accessor that you can **move again**. You create it one time, and then you move it with
|
|
14
971
|
`at(entity)`.
|
|
15
972
|
|
|
16
973
|
```ts
|
|
@@ -44,9 +1001,9 @@ and the read-only name has the `Read` suffix. There is one constraint. A compone
|
|
|
44
1001
|
named `at` collides with the method of the cursor. Creation of the cursor rejects that component,
|
|
45
1002
|
and the message says so.
|
|
46
1003
|
|
|
47
|
-
See [refs and cursors](docs/api/refs.md#cursors
|
|
1004
|
+
See [refs and cursors](docs/api/refs.md#cursors-many-entities-by-id).
|
|
48
1005
|
|
|
49
|
-
### Added
|
|
1006
|
+
### Added. `ECS_ERROR.ARCHETYPE_ROW_INVARIANT`
|
|
50
1007
|
|
|
51
1008
|
This error reports that the row bookkeeping of an archetype does not agree with its backing columns.
|
|
52
1009
|
There are three causes. A reserve did not give the capacity that the engine asked for. A restore
|
|
@@ -56,7 +1013,7 @@ invariant, and not a mistake by the caller. That is what makes it different from
|
|
|
56
1013
|
`STORE_CAP_EXCEEDED`, which is the allocator that refuses a legitimate grow. Two development
|
|
57
1014
|
assertions that reported the general `COMPONENT_NOT_REGISTERED` now use it.
|
|
58
1015
|
|
|
59
|
-
### Changed
|
|
1016
|
+
### Changed. Structural churn, system dispatch, and fragmented iteration
|
|
60
1017
|
|
|
61
1018
|
No signature changed on a function that exists, and no result changed. Each entry below is a change
|
|
62
1019
|
to the internal mechanism. What moved:
|
|
@@ -100,9 +1057,9 @@ to the internal mechanism. What moved:
|
|
|
100
1057
|
array is a property store, and V8 does not remove that store for an array that is already empty.
|
|
101
1058
|
This ran one time for each `update()` call, and most phases emit no event.
|
|
102
1059
|
|
|
103
|
-
### Changed
|
|
1060
|
+
### Changed. Diagnostic vocabulary catch-up (the deferred snake_case remnants)
|
|
104
1061
|
|
|
105
|
-
- **
|
|
1062
|
+
- **Breaking (diagnostics):** `ECS.memoryPlan.source` now reports `"maxBytes"` instead of
|
|
106
1063
|
`"max_bytes"` for the arm with the explicit byte cap. The name now agrees with the option key
|
|
107
1064
|
that it names. Every other arm already agreed: `budget`, `heap`, `shared`, `wasm`, `allocator`,
|
|
108
1065
|
and `default`.
|
|
@@ -111,7 +1068,7 @@ to the internal mechanism. What moved:
|
|
|
111
1068
|
`wasm.maximumPages`, `wasm.initialPages`, and `capBytesHint`), and not by the snake_case
|
|
112
1069
|
spellings from before 0.4.
|
|
113
1070
|
|
|
114
|
-
### Fixed
|
|
1071
|
+
### Fixed. Row-plane and schedule-slot correctness under the new caches
|
|
115
1072
|
|
|
116
1073
|
- **A grow that throws no longer leaves the row plane on a buffer that the engine released.** The
|
|
117
1074
|
reserve grows the entity-id array on the heap before it asks the store to grow the columns. So
|
|
@@ -140,7 +1097,7 @@ to the internal mechanism. What moved:
|
|
|
140
1097
|
of the new system, and it gave no signal. The engine now recycles a slot only outside a running
|
|
141
1098
|
drive. Between drives the slots recycle as before, so the array stays bounded.
|
|
142
1099
|
|
|
143
|
-
### Note
|
|
1100
|
+
### Note. The lookup of a field name stays as it is
|
|
144
1101
|
|
|
145
1102
|
`getField` resolves a field name through the `_fieldIndex[cid][field]` table. The engine builds that
|
|
146
1103
|
table with `Object.create(null)`, which puts it in dictionary mode. The investigation covered three
|
|
@@ -172,11 +1129,11 @@ investigation. To remove the lookup, the caller must hold the ordinal of the fie
|
|
|
172
1129
|
against a list of the files to exclude, and the local bench and oracle harnesses would have
|
|
173
1130
|
shipped as soon as git tracked them.
|
|
174
1131
|
|
|
175
|
-
## [0.5.3]
|
|
1132
|
+
## [0.5.3] - 2026-07-09
|
|
176
1133
|
|
|
177
|
-
### Fixed
|
|
1134
|
+
### Fixed. Heap columns are again on the fast element-access path of V8
|
|
178
1135
|
|
|
179
|
-
- The pure-TS **heap profile** (`heapArraybufferAllocator
|
|
1136
|
+
- The pure-TS **heap profile** (`heapArraybufferAllocator`, the default backing)
|
|
180
1137
|
now reserves its store as a **fixed, non-resizable `ArrayBuffer`** at the full
|
|
181
1138
|
cap. Before this release it used a growable buffer, and it made that buffer
|
|
182
1139
|
larger with `.resize()`. V8 has no fast element-access path for a TypedArray
|
|
@@ -190,44 +1147,44 @@ investigation. To remove the lookup, the caller must hold the ordinal of the fie
|
|
|
190
1147
|
256 MiB default cap, which is equivalent to the old resizable buffer.
|
|
191
1148
|
Growth remains in place: the store relocates columns within the pre-reserved
|
|
192
1149
|
buffer, so the buffer identity never changes and every existing view stays
|
|
193
|
-
valid
|
|
1150
|
+
valid.
|
|
194
1151
|
`isInPlace: true` and the entity-index-hoist-across-grow invariant hold
|
|
195
1152
|
unchanged. The store keys its tail cursor off the header `capacity` (the
|
|
196
1153
|
logical high-water) rather than `buffer.byteLength` (now always the cap).
|
|
197
|
-
- Only the heap backing changed. The `growable_sab`
|
|
1154
|
+
- Only the heap backing changed. The `growable_sab` and `wasm_memory` backings
|
|
198
1155
|
keep their resizable buffers and page-rounded tail layout byte-for-byte
|
|
199
|
-
(their determinism
|
|
1156
|
+
(their determinism and layout goldens are unchanged).
|
|
200
1157
|
|
|
201
|
-
## [0.5.2]
|
|
1158
|
+
## [0.5.2] - 2026-07-08
|
|
202
1159
|
|
|
203
|
-
### Added
|
|
1160
|
+
### Added. A guards-on build and an explicit dev entry
|
|
204
1161
|
|
|
205
|
-
- **`@oasys/oecs/dev
|
|
206
|
-
guards left **on**, for a direct guards-on import (browser
|
|
1162
|
+
- **`@oasys/oecs/dev`**, the same public API as `@oasys/oecs` with the `__DEV__`
|
|
1163
|
+
guards left **on**, for a direct guards-on import (browser or CDN, quick debugging,
|
|
207
1164
|
or bundlers that don't auto-select conditions).
|
|
208
|
-
- **`development` export condition
|
|
1165
|
+
- **`development` export condition**, dev-mode bundlers (`vite dev`,
|
|
209
1166
|
`webpack --mode development`) now resolve `@oasys/oecs` (and every subpath) to a
|
|
210
|
-
guards-on build automatically
|
|
1167
|
+
guards-on build automatically. Production-mode builds resolve to the stripped
|
|
211
1168
|
build as before.
|
|
212
|
-
- npm now ships a **dual build** (`scripts/build.mjs`): the default `*.js
|
|
1169
|
+
- npm now ships a **dual build** (`scripts/build.mjs`): the default `*.js` and `*.cjs`
|
|
213
1170
|
are the stripped production artifacts (unchanged), alongside new guards-on
|
|
214
|
-
`*.development.js
|
|
1171
|
+
`*.development.js` and `*.development.cjs`.
|
|
215
1172
|
- New guide: [Development guards & production builds](docs/PRODUCTION.md).
|
|
216
1173
|
|
|
217
|
-
### Changed
|
|
1174
|
+
### Changed. Dev-guard default is now production on JSR or Deno
|
|
218
1175
|
|
|
219
|
-
- Raw-source (JSR
|
|
220
|
-
guards off
|
|
1176
|
+
- Raw-source (JSR and Deno) consumers now default to `__DEV__ = false` (production, with the
|
|
1177
|
+
guards off and no cost in each frame), which matches the npm default. Previously the raw path
|
|
221
1178
|
defaulted to guards-on. To enable the guards while developing on Deno, set
|
|
222
1179
|
`globalThis.__DEV__ = true` **before the first import**. The `globalThis.__DEV__`
|
|
223
|
-
override is unchanged
|
|
1180
|
+
override is unchanged. Only the default flipped. (`dev_flag.ts`)
|
|
224
1181
|
|
|
225
|
-
## [0.5.1]
|
|
1182
|
+
## [0.5.1] - 2026-07-06
|
|
226
1183
|
|
|
227
|
-
### Changed (breaking)
|
|
1184
|
+
### Changed (breaking). One attach grammar
|
|
228
1185
|
|
|
229
1186
|
`addComponents` and `template` now take the same callable-bundle varargs as `spawnBundle`
|
|
230
|
-
and `ctx.commands.spawn`
|
|
1187
|
+
and `ctx.commands.spawn` and `add`, replacing the `{ def, values }[]` entry-object array, one
|
|
231
1188
|
grammar across every authoring surface:
|
|
232
1189
|
|
|
233
1190
|
```ts
|
|
@@ -243,17 +1200,17 @@ To migrate: drop the array brackets, wrap a valued entry in its def's call
|
|
|
243
1200
|
(`{ def: X, values: V }` → `X(V)`), and leave a bare entry bare (`{ def: X }` → `X`).
|
|
244
1201
|
|
|
245
1202
|
- Each item is schema-checked against its **own** def via the `StrictBundles` mapped tuple
|
|
246
|
-
(`{ [K in keyof Items]: … }`)
|
|
1203
|
+
(`{ [K in keyof Items]: … }`), a misspelled or cross-component field, including a
|
|
247
1204
|
hand-written raw `{ def, values }` literal, is a compile error. `spawnBundle` gains this
|
|
248
1205
|
per-item checking (it previously had none).
|
|
249
|
-
- `ctx.commands.spawn`
|
|
1206
|
+
- `ctx.commands.spawn` and `ctx.commands.add` now schema-check their bundle values in
|
|
250
1207
|
declared-access systems as well (the `DeclaredBundleOrDef` type distributes over the
|
|
251
|
-
declared add set)
|
|
252
|
-
- The `TemplateEntry`
|
|
253
|
-
entry-object grammar). The host command seam (`HostCommandQueue.spawn
|
|
1208
|
+
declared add set). A permissive context and an `exclusive` context stay loose, as before.
|
|
1209
|
+
- The `TemplateEntry` and `TemplateEntries` public types are removed (they encoded the retired
|
|
1210
|
+
entry-object grammar). The host command seam (`HostCommandQueue.spawn`, the record and replay
|
|
254
1211
|
and editor-undo transport) deliberately keeps its entry-object + complete-values shape.
|
|
255
1212
|
|
|
256
|
-
### Changed
|
|
1213
|
+
### Changed. API vocabulary consistency
|
|
257
1214
|
|
|
258
1215
|
Cheap alignments from a public-API vocabulary audit that followed the grammar unification:
|
|
259
1216
|
|
|
@@ -262,101 +1219,101 @@ Cheap alignments from a public-API vocabulary audit that followed the grammar un
|
|
|
262
1219
|
- `HostCommandQueue.pending()` is now a `pending` getter (matching every other count accessor).
|
|
263
1220
|
- `ReadonlyEntityIdArray` → `ReadonlyEntityIDArray` (acronym casing, matching `EntityID`).
|
|
264
1221
|
- The entity-id parameter is now uniformly `entityId` across the core surface (ECS lifecycle,
|
|
265
|
-
host-command queue, `ObserverFn`)
|
|
1222
|
+
host-command queue, `ObserverFn`). The `HostCommand` wire-format field stays `eid`.
|
|
266
1223
|
- Source-compatible widenings: `ecs.despawn`, `ecs.removeSystem`, and the `HostCommandQueue`
|
|
267
1224
|
mutators now return `this` for chaining.
|
|
268
1225
|
|
|
269
|
-
The `ref`
|
|
270
|
-
(`ctx.ref(def, entityId)`): these are the outside-iteration members of the `cols.mut`
|
|
1226
|
+
The `ref` and `refRead` argument order was reviewed and **deliberately kept** def-first
|
|
1227
|
+
(`ctx.ref(def, entityId)`): these are the outside-iteration members of the `cols.mut` and
|
|
271
1228
|
`cols.read` column-cursor family, so def-first is the cursor convention, not an inconsistency
|
|
272
|
-
to fix
|
|
1229
|
+
to fix, flipping it would align with `getField` while breaking alignment with `cols.mut`.
|
|
273
1230
|
Documented as such (`refs.md`, `queries.md`) rather than flipped.
|
|
274
1231
|
|
|
275
|
-
### Changed (breaking)
|
|
1232
|
+
### Changed (breaking). Host-write-seam verb grammar
|
|
276
1233
|
|
|
277
1234
|
The host-write-seam handles are namespaced command buffers, so they drop the component noun
|
|
278
|
-
to match `ctx.commands.add`
|
|
279
|
-
`spawn
|
|
1235
|
+
to match `ctx.commands.add` and `remove`, and their own already-bare
|
|
1236
|
+
`spawn`, `despawn`, `disable`, `enable` and `setField`:
|
|
280
1237
|
|
|
281
|
-
- `HostCommandQueue.addComponent`
|
|
282
|
-
- `Editor` and `TransactionBuilder` `.addComponent`
|
|
1238
|
+
- `HostCommandQueue.addComponent` and `removeComponent` → `add` and `remove`.
|
|
1239
|
+
- `Editor` and `TransactionBuilder` `.addComponent` and `.removeComponent` → `add` and `remove`
|
|
283
1240
|
(the two surfaces are designed to match, so they move together).
|
|
284
1241
|
- The editor extension's entity-id parameters and its `FieldReader` type now read `entityId`,
|
|
285
1242
|
completing the core's `eid` → `entityId` pass.
|
|
286
1243
|
|
|
287
|
-
The wire-format `kind` discriminants (`"add_component"`
|
|
288
|
-
codecs, and the `HostCommand` record's `eid` field are unchanged
|
|
1244
|
+
The wire-format `kind` discriminants (`"add_component"` and `"remove_component"`), the ring
|
|
1245
|
+
codecs, and the `HostCommand` record's `eid` field are unchanged, transport vocabulary.
|
|
289
1246
|
|
|
290
|
-
### Changed (breaking)
|
|
1247
|
+
### Changed (breaking). `ctx.getResource`
|
|
291
1248
|
|
|
292
1249
|
The in-system resource getter is now `ctx.getResource(key)` (was the verb-less `ctx.resource(key)`),
|
|
293
|
-
matching its flat-surface siblings `setResource
|
|
294
|
-
`getField
|
|
295
|
-
surface verbs every accessor
|
|
296
|
-
`remove`
|
|
1250
|
+
matching its flat-surface siblings `setResource`, `removeResource` and `hasResource` and the
|
|
1251
|
+
`getField`, `setField` and `hasComponent` convention. The rule is now explicit: the flat `ctx`
|
|
1252
|
+
surface verbs every accessor. The grouped `ecs.resources` facade drops the noun (`get`, `set`,
|
|
1253
|
+
`remove` and `has`) because its receiver already names it. `ConditionContext` (run-condition
|
|
297
1254
|
predicates) moves in lockstep.
|
|
298
1255
|
|
|
299
1256
|
### Fixed
|
|
300
1257
|
|
|
301
|
-
- The immediate host spawn family (`spawn
|
|
302
|
-
when called from inside a system body
|
|
1258
|
+
- The immediate host spawn family (`spawn`, `spawnBundle` and `spawnMany`) now throws in DEV
|
|
1259
|
+
when called from inside a system body, redirecting to `ctx.commands.spawn`, like every
|
|
303
1260
|
other immediate host structural mutator. Previously it was silently unguarded (the archetype
|
|
304
|
-
iteration guard does not cover the append path), a live mid-iteration footgun
|
|
1261
|
+
iteration guard does not cover the append path), a live mid-iteration footgun. Its guard
|
|
305
1262
|
docstring's "one rule for every host mutator" claim is now true.
|
|
306
1263
|
- Added explicit public `QueryCache` cache-map type annotations so JSR publish passes
|
|
307
1264
|
slow-type validation and can generate package declarations cleanly.
|
|
308
1265
|
|
|
309
|
-
## [0.5.0]
|
|
1266
|
+
## [0.5.0] - 2026-07-06
|
|
310
1267
|
|
|
311
|
-
### Changed (breaking)
|
|
1268
|
+
### Changed (breaking). Lifecycle & naming unification
|
|
312
1269
|
|
|
313
|
-
One vocabulary across host, commands, and access declarations
|
|
314
|
-
timing (host = immediate, `ctx.commands` = deferred). Hard renames, no deprecation aliases
|
|
315
|
-
|
|
316
|
-
|
|
1270
|
+
One vocabulary across host, commands, and access declarations. The receiver now implies the
|
|
1271
|
+
timing (host = immediate, `ctx.commands` = deferred). Hard renames, no deprecation aliases.
|
|
1272
|
+
See [docs/MIGRATION-0.4-to-0.5.md](docs/MIGRATION-0.4-to-0.5.md) for the complete map of the
|
|
1273
|
+
renames and the removals:
|
|
317
1274
|
|
|
318
1275
|
| 0.4 | 0.5 |
|
|
319
1276
|
| --- | --- |
|
|
320
|
-
| `ecs.createEntity()`
|
|
1277
|
+
| `ecs.createEntity()` and `ecs.createEntity(template, overrides?)` | `ecs.spawn()` and `ecs.spawn(template, overrides?)` |
|
|
321
1278
|
| `ecs.createEntities(template, count)` | `ecs.spawnMany(template, count, overrides?)` |
|
|
322
|
-
| `ecs.destroyEntity(e)` *(deferred)* | `ecs.despawn(e)
|
|
1279
|
+
| `ecs.destroyEntity(e)` *(deferred)* | `ecs.despawn(e)`, **now immediate** |
|
|
323
1280
|
| `ctx.createEntity()` | `ctx.commands.spawn()` |
|
|
324
1281
|
| `ctx.destroyEntity(e)` | `ctx.commands.despawn(e)` |
|
|
325
1282
|
| `ctx.addComponent(e, def, values?)` | `ctx.commands.add(e, def, values)` or `ctx.commands.add(e, def({ … }))` |
|
|
326
1283
|
| `ctx.removeComponent(e, def)` | `ctx.commands.remove(e, def)` |
|
|
327
|
-
| `ctx.disable(e)`
|
|
328
|
-
| `sourcesOf(def, tgt)` | `sourcesOf(tgt, def)
|
|
1284
|
+
| `ctx.disable(e)` and `ctx.enable(e)` | `ctx.commands.disable(e)` and `ctx.commands.enable(e)` |
|
|
1285
|
+
| `sourcesOf(def, tgt)` | `sourcesOf(tgt, def)`, matches `targetOf` and `targetsOf` |
|
|
329
1286
|
| `query.count()` | `query.entityCount` (getter, beside `archetypeCount`) |
|
|
330
|
-
| `WorldRestoreError`
|
|
1287
|
+
| `WorldRestoreError` and `WORLD_SNAPSHOT_VERSION` | `ECSRestoreError` and `ECS_SNAPSHOT_VERSION` |
|
|
331
1288
|
|
|
332
|
-
- **Host `despawn` is immediate
|
|
1289
|
+
- **Host `despawn` is immediate**, `ecs.despawn(e); ecs.isAlive(e)` is `false` on the next
|
|
333
1290
|
line. This removes the inconsistency: host `addComponent` was immediate, but destroy was
|
|
334
1291
|
buffered.
|
|
335
|
-
**Observer note:** like every immediate op, host `despawn` fires no *structural* observers
|
|
1292
|
+
**Observer note:** like every immediate op, host `despawn` fires no *structural* observers.
|
|
336
1293
|
`onRemove` no longer sees host-despawned entities (it did at 0.4, when host destroy was
|
|
337
1294
|
deferred). Observer-driven consumers, including the `reactive-sync` map bridges, only see
|
|
338
1295
|
despawns that go through `ctx.commands.despawn` or the host-command seam. (`onSet` is
|
|
339
|
-
receiver-blind
|
|
1296
|
+
receiver-blind, derived change detection sees host `setField` writes as always.)
|
|
340
1297
|
- **Every immediate host structural mutator throws in dev when called from inside a system
|
|
341
|
-
body
|
|
342
|
-
`batchAddComponent
|
|
1298
|
+
body**, `despawn`, `addComponent` and `addComponents`, `removeComponent` and `removeComponents`,
|
|
1299
|
+
`batchAddComponent` and `batchRemoveComponent`, `disable` and `enable`, each error pointing at its
|
|
343
1300
|
`ctx.commands` equivalent. Mid-system these ops can move rows a running query is walking and
|
|
344
|
-
are invisible to observers
|
|
1301
|
+
are invisible to observers. Previously only `despawn` was guarded wholesale (the others were
|
|
345
1302
|
caught only when they touched the archetype being iterated). Cross-world host mutation from
|
|
346
|
-
another world's system is unaffected
|
|
347
|
-
- **The bare deferred duplicates on `ctx` are removed
|
|
1303
|
+
another world's system is unaffected, the guard is scoped to the mutated world.
|
|
1304
|
+
- **The bare deferred duplicates on `ctx` are removed**, `ctx.addComponent`,
|
|
348
1305
|
`ctx.removeComponent`, `ctx.disable`, `ctx.enable` join the already-removed
|
|
349
|
-
`ctx.createEntity`
|
|
1306
|
+
`ctx.createEntity` and `ctx.destroyEntity`. `ctx.commands` is now the *only* deferred surface,
|
|
350
1307
|
completing the receiver-implies-timing rule with zero exceptions. `ctx.commands.add` gains
|
|
351
1308
|
the explicit complete-values shape (`ctx.commands.add(e, Pos, { x: 0, y: 0 })`) the removed
|
|
352
1309
|
`ctx.addComponent` carried, so compile-checked complete attaches survive the move.
|
|
353
|
-
`ctx.isDisabled` stays (immediate read), as do the immediate sparse
|
|
354
|
-
- **`sourcesOf` canonicalized to `(entity, def)`** on `ecs.relations` and `SystemContext
|
|
355
|
-
|
|
356
|
-
- **The package root is now a curated, explicit export list
|
|
1310
|
+
`ctx.isDisabled` stays (immediate read), as do the immediate sparse and relation ops.
|
|
1311
|
+
- **`sourcesOf` canonicalized to `(entity, def)`** on `ecs.relations` and `SystemContext`,
|
|
1312
|
+
It was the one arg-order outlier on the relation surface.
|
|
1313
|
+
- **The package root is now a curated, explicit export list**, `export *` no longer flattens the
|
|
357
1314
|
whole core barrel, so future barrel additions cannot silently widen the public API. A checked-in
|
|
358
1315
|
public-API snapshot test makes any surface change an explicit diff in review.
|
|
359
|
-
- **Internal
|
|
1316
|
+
- **Internal and tooling symbols moved to `@oasys/oecs/internal`** (explicitly **unstable, no semver
|
|
360
1317
|
guarantees**): the packed-EntityID codec (`createEntityId`, `getEntityGeneration`, `MAX_INDEX`,
|
|
361
1318
|
`MAX_GENERATION`, `MAX_LIVE_GENERATION`, `RETIRED_GENERATION`, `MAX_ENTITY_ID`), the SAB
|
|
362
1319
|
command-ring transport (`HostCommandDispatcher`, `ring*Codec`, `HOST_COMMAND_PAYLOAD_BYTES`),
|
|
@@ -365,150 +1322,150 @@ rename/removal map:
|
|
|
365
1322
|
|
|
366
1323
|
### Added
|
|
367
1324
|
|
|
368
|
-
- **`addComponent` bundle overload
|
|
369
|
-
with the usual zero-fill semantics
|
|
1325
|
+
- **`addComponent` bundle overload**, `ecs.addComponent(e, Pos({ x: 1 }))` accepts a bundle
|
|
1326
|
+
with the usual zero-fill semantics. The explicit `(e, def, values)` form stays
|
|
370
1327
|
complete-values, so a typo'd or missing field is still a compile error.
|
|
371
|
-
- **`spawnMany` typed template + shared overrides
|
|
1328
|
+
- **`spawnMany` typed template + shared overrides**, bulk spawn takes the same typed
|
|
372
1329
|
`Template<Defs>` as `spawn` plus one optional `TemplateOverrides<Defs>` object applied to
|
|
373
1330
|
every row (contiguous batches use one `fill` per overridden column).
|
|
374
|
-
- **JSDoc `@example` on the core surface
|
|
375
|
-
`query`, `registerSystem`, `startup`, `update`, `ctx.emit`
|
|
1331
|
+
- **JSDoc `@example` on the core surface**, `registerComponent`, `spawn`, `addComponent`,
|
|
1332
|
+
`query`, `registerSystem`, `startup`, `update`, `ctx.emit` and `ctx.read`,
|
|
376
1333
|
`events.register`, `resources.register` now carry hover-visible examples.
|
|
377
|
-
- **Component debug names
|
|
1334
|
+
- **Component debug names**, `registerComponent(schema, { name: "Pos" })` (and the sparse
|
|
378
1335
|
sibling) records a diagnostic label, so access-violation and liveness errors read
|
|
379
1336
|
`'Pos' (component 5)` instead of leaving you to count registration order
|
|
380
1337
|
(`ComponentRegisterOptions`).
|
|
381
|
-
- **Total probes + `tryGetField
|
|
1338
|
+
- **Total probes + `tryGetField`**, `hasComponent`, `hasSparse` and `relations.has` now return
|
|
382
1339
|
`false` for a dead entity instead of dev-throwing (a "has" probe is exactly the call made to
|
|
383
|
-
avoid dead entities)
|
|
1340
|
+
avoid dead entities). `ecs.tryGetField(e, def, field)` returns `undefined` for a dead entity or
|
|
384
1341
|
missing component, and `ctx.tryGetField` mirrors it inside systems (declared-read checked).
|
|
385
|
-
- **Plural host mutators chain
|
|
1342
|
+
- **Plural host mutators chain**, `addComponents`, `removeComponents`, `batchAddComponent`,
|
|
386
1343
|
`batchRemoveComponent` return `this` (previously `void`), matching their singular siblings.
|
|
387
|
-
- **`Query.firstEntity()`
|
|
388
|
-
hand-rolled `forEach` + capture
|
|
389
|
-
- **Host-side `ecs.refRead(def, e)
|
|
1344
|
+
- **`Query.firstEntity()` and `Query.singleEntity()`**, singleton reads (player, camera) without a
|
|
1345
|
+
hand-rolled `forEach` + capture. `singleEntity` dev-throws `QUERY_NOT_SINGLETON` on 0 or >1.
|
|
1346
|
+
- **Host-side `ecs.refRead(def, e)`**, whole-component read-only view, parity with
|
|
390
1347
|
`ctx.refRead`.
|
|
391
|
-
- **Run-condition combinators
|
|
1348
|
+
- **Run-condition combinators**, `not()`, `allOf()` and `anyOf()`, merging the operands' declared
|
|
392
1349
|
read surfaces.
|
|
393
|
-
- **Editor change notification
|
|
394
|
-
`canUndo`
|
|
395
|
-
- **`using` support
|
|
396
|
-
- **Write-seam lifecycle
|
|
1350
|
+
- **Editor change notification**, `editor.onChange(cb)` (fires on commit, undo, redo and clear) plus
|
|
1351
|
+
`canUndo` and `canRedo` getters. No more per-frame `depths()` polling.
|
|
1352
|
+
- **`using` support**, `ObserverHandle` implements `Symbol.dispose`.
|
|
1353
|
+
- **Write-seam lifecycle**, `uninstallHostCommandSeam(world, queue)`,
|
|
397
1354
|
`HostCommandQueue.clear()`, `HostCommandDispatcher.off(opCode)`,
|
|
398
1355
|
`HostCommandRecorder.snapshotLog()` (stable deep copy).
|
|
399
|
-
- **`VERSION`** export and a `"./package.json"` export
|
|
1356
|
+
- **`VERSION`** export and a `"./package.json"` export. `engines: { node: ">=20" }` and a README
|
|
400
1357
|
runtime note (resizable `ArrayBuffer`).
|
|
401
1358
|
- Root re-exports so failure modes are nameable without extra entry points:
|
|
402
|
-
`StoreRestoreError`, `SabUnavailableError`, `TypedArrayTag
|
|
403
|
-
`shallow` (moved from `/reactive-sync`, which re-exports for compat)
|
|
404
|
-
zero-arg Solid-parity overload
|
|
405
|
-
- **`FrameStepper
|
|
406
|
-
`play()
|
|
407
|
-
for tests and non-browser hosts), explicit `step()
|
|
1359
|
+
`StoreRestoreError`, `SabUnavailableError`, `TypedArrayTag`. `/reactive` now exports `Eq` and
|
|
1360
|
+
`shallow` (moved from `/reactive-sync`, which re-exports for compat). `signal()` gains the
|
|
1361
|
+
zero-arg Solid-parity overload. `SingletonSyncOptions.eq`.
|
|
1362
|
+
- **`FrameStepper`**, optional host-side driver over the authoritative `ecs.update(dt)`:
|
|
1363
|
+
`play()`, `pause()` and `toggle()` on `requestAnimationFrame` (injectable `requestFrame` and `cancelFrame`
|
|
1364
|
+
for tests and non-browser hosts), explicit `step()` and `stepFrames()` for debuggers, editors, and
|
|
408
1365
|
rollback playback, and a `maxDt` clamp (default 0.25 s) so a resumed background tab doesn't feed
|
|
409
1366
|
the whole suspension into the accumulator as one delta. Validation throws `INVALID_FRAME_STEP`.
|
|
410
|
-
- **`ObserverConfig.name
|
|
411
|
-
`observer_fired.observer` field (the role a system's `name` plays)
|
|
1367
|
+
- **`ObserverConfig.name`**, diagnostic label surfaced as the frame trace's
|
|
1368
|
+
`observer_fired.observer` field (the role a system's `name` plays). Observe-only, never affects
|
|
412
1369
|
`stateHash` or dispatch order. Unnamed observers fall back to `observer(<component debug name>)`
|
|
413
1370
|
when the component was registered with a name, else `observer(<cid>)`.
|
|
414
|
-
- **`ECSOptions.onWarn
|
|
1371
|
+
- **`ECSOptions.onWarn`**, injectable sink for dev-mode engine diagnostics (currently the
|
|
415
1372
|
schedule's dropped-ordering-edge warning and the `ECSOptions` unknown-key warning),
|
|
416
1373
|
defaulting to `console.warn`. Replaces the internal `src/log` singleton, which is deleted.
|
|
417
|
-
- **Editor `fieldHandle` `read` thunk is optional
|
|
1374
|
+
- **Editor `fieldHandle` `read` thunk is optional**, defaults to `Editor.committedField`.
|
|
418
1375
|
|
|
419
1376
|
### Fixed
|
|
420
1377
|
|
|
421
|
-
- **Host iteration guard (`STRUCTURAL_DURING_ITERATION`)
|
|
422
|
-
a host-side `forEach
|
|
1378
|
+
- **Host iteration guard (`STRUCTURAL_DURING_ITERATION`)**, with host `despawn` now immediate,
|
|
1379
|
+
a host-side `forEach` and `eachChunk` callback that despawned (or transitioned and toggled) an entity of
|
|
423
1380
|
the archetype it was visiting silently skipped entities via the row swap-remove. Row-removing
|
|
424
1381
|
ops on an archetype a live dense walk is standing in now throw in dev, *before* any mutation
|
|
425
1382
|
lands (the transition path checks ahead of the destination append, so no dual-residency
|
|
426
1383
|
half-state). Collect ids during the walk and mutate after it. Mutating archetypes the walk is
|
|
427
|
-
*not* currently visiting stays legal
|
|
428
|
-
- **Cross-world despawn false positive
|
|
429
|
-
longer trips the in-system despawn guard (the accessCheck span is process-global
|
|
1384
|
+
*not* currently visiting stays legal, the fresh-snapshot machinery still covers those.
|
|
1385
|
+
- **Cross-world despawn false positive**, `worldB.despawn(e)` from inside world A's system no
|
|
1386
|
+
longer trips the in-system despawn guard (the accessCheck span is process-global, the guard now
|
|
430
1387
|
also requires *this* world to be mid-schedule). Driving a second world from a system
|
|
431
|
-
mutates it host-style, which is safe
|
|
1388
|
+
mutates it host-style, which is safe. B is not iterating. Unnamed systems in the guard message
|
|
432
1389
|
now render as `system_<id>` instead of `'?'`.
|
|
433
|
-
- **Frame trace records every deferred command
|
|
434
|
-
forms bypassed the `commandQueued` trace hook, so host-command-seam adds
|
|
1390
|
+
- **Frame trace records every deferred command**, the removed bare `ctx.*` deferred
|
|
1391
|
+
forms bypassed the `commandQueued` trace hook, so host-command-seam adds, removes and toggles (and
|
|
435
1392
|
any system using the bare forms) were invisible to an attached `FrameTraceSink` while their
|
|
436
|
-
spawns
|
|
1393
|
+
spawns and despawns were visible. With `ctx.commands` as the only deferred surface every queued
|
|
437
1394
|
command is traced, and `ctx.commands.spawn` now also traces each bundle attach it queues
|
|
438
1395
|
(previously only the spawn itself).
|
|
439
|
-
- **Stale deferred-attach docs
|
|
440
|
-
deferred add path does not zero-fill omitted fields (NaN readback)
|
|
1396
|
+
- **Stale deferred-attach docs**, `host_commands.ts` and the host-write-seam page claimed the
|
|
1397
|
+
deferred add path does not zero-fill omitted fields (NaN readback). Every attach path
|
|
441
1398
|
zero-fills (`writeFields`'s `?? 0`). The complete-values requirement on
|
|
442
|
-
`SpawnEntry` is documented as what it is
|
|
443
|
-
|
|
1399
|
+
`SpawnEntry` is documented as what it is, explicit intent in a reified, replayable record.
|
|
1400
|
+
The observer docs now scope "immediate ops fire no observers" to *structural* observers
|
|
444
1401
|
(`onSet` is derived change detection and sees host `setField` writes).
|
|
445
|
-
- **`ecs.refRead
|
|
446
|
-
`TypeError` from the ref internals
|
|
1402
|
+
- **`ecs.refRead`, `ctx.ref` and `ctx.refRead` on a missing component or tag def**, threw a raw
|
|
1403
|
+
`TypeError` from the ref internals. Now a dev `ECSError` (`COMPONENT_NOT_REGISTERED`) naming the
|
|
447
1404
|
op and component, matching `getField`. Host `refRead`'s docstring now states the single-
|
|
448
1405
|
expression lifetime rule (any immediate structural mutation can row-swap under a held ref).
|
|
449
|
-
- **Editor: aborted transactions no longer poison undo
|
|
1406
|
+
- **Editor: aborted transactions no longer poison undo**, `transaction(tx => …)` staged its
|
|
450
1407
|
`setField` shadow writes into the editor's shared map at build time, so a build callback that
|
|
451
1408
|
threw left phantom pending values behind and seeded the *next* edit's undo inverse with a value
|
|
452
1409
|
the world never held. Staging is now transaction-local and merges only on commit.
|
|
453
|
-
- **Editor: `pendingField` self-resolves for dead slots
|
|
454
|
-
(or removed component) echoed its stale value forever and leaked
|
|
1410
|
+
- **Editor: `pendingField` self-resolves for dead slots**, a shadow entry for a despawned entity
|
|
1411
|
+
(or removed component) echoed its stale value forever and leaked. The reconcile-on-read now
|
|
455
1412
|
prunes it and returns `undefined`.
|
|
456
|
-
- **JSR
|
|
1413
|
+
- **JSR or Deno consumers no longer break on the `__DEV__` global**, shipped source now reads a
|
|
457
1414
|
guarded `DEV` flag (`src/dev_flag.ts`) that constant-folds in the npm bundle and defaults to
|
|
458
1415
|
dev-on for raw-source consumers (`globalThis.__DEV__ = false` opts out).
|
|
459
|
-
- **Error experience
|
|
460
|
-
(index + generation, with context)
|
|
461
|
-
category instead of overloading `*_NOT_REGISTERED
|
|
462
|
-
name the key and hint the registration call
|
|
1416
|
+
- **Error experience**, every `ENTITY_NOT_ALIVE` names the operation and decodes the packed id
|
|
1417
|
+
(index + generation, with context). System access violations use the new `ACCESS_UNDECLARED`
|
|
1418
|
+
category instead of overloading `*_NOT_REGISTERED`. Resource and event "not registered" messages
|
|
1419
|
+
name the key and hint the registration call. Messages no longer reference pre-0.4 snake_case
|
|
463
1420
|
option names or private tracker issue numbers.
|
|
464
|
-
- **Packaging
|
|
1421
|
+
- **Packaging**, per-entry `.d.cts` and explicit-extension declaration specifiers
|
|
465
1422
|
(`attw --pack` fully green: node10/node16/bundler across all eight entry points, was
|
|
466
|
-
masquerading + resolution errors)
|
|
467
|
-
tarball ships `CHANGELOG.md
|
|
468
|
-
- **Type-level closures
|
|
469
|
-
now accepted)
|
|
470
|
-
error
|
|
471
|
-
`pairsOf`
|
|
1423
|
+
masquerading + resolution errors). `typesVersions` for `moduleResolution: node10` subpaths. Npm
|
|
1424
|
+
tarball ships `CHANGELOG.md`. `@internal` editor internals no longer leak into published types.
|
|
1425
|
+
- **Type-level closures**, `EventShape<S>` homomorphic bound (interface-declared event schemas
|
|
1426
|
+
now accepted). `RelationOptions` is a union so `{ exclusive: true, multi: true }` is a compile
|
|
1427
|
+
error. `ResourceKey`'s phantom is a unique symbol (no `.__phantom` in autocomplete)
|
|
1428
|
+
`pairsOf` and `sourcesOfAny` return readonly tuples. `SystemConfig.fn` optional when
|
|
472
1429
|
`backendHandle` is present.
|
|
473
|
-
- Dev-mode diagnostics: ownerless `computed()`
|
|
474
|
-
unknown keys
|
|
1430
|
+
- Dev-mode diagnostics: ownerless `computed()` and `onCleanup()` warn (kernel). ECSOptions warns on
|
|
1431
|
+
unknown keys. `runIfResourceEq` warns on object-valued `expected` (reference-identity `===`)
|
|
475
1432
|
`runEveryNTicks` validation throws `ECSError` (`INVALID_RUN_CONDITION`).
|
|
476
|
-
- **Docs standardized on the `ecs` receiver
|
|
1433
|
+
- **Docs standardized on the `ecs` receiver**. README, GETTING_STARTED, BEST_PRACTICES, the
|
|
477
1434
|
api reference, and every in-source JSDoc example now spell `const ecs = new ECS()`
|
|
478
1435
|
(with the `World*` names renamed to `ECS*`, "world" survives only as prose). The
|
|
479
1436
|
host-write-seam docs now explain *why* `queue.spawn` takes complete-value `spawnEntry`s
|
|
480
|
-
rather than zero-filling bundles: commands are a reified, replayable record
|
|
481
|
-
values are explicit intent legible to replay, not a correctness need
|
|
1437
|
+
rather than zero-filling bundles: commands are a reified, replayable record, complete
|
|
1438
|
+
values are explicit intent legible to replay, not a correctness need. The deferred add path
|
|
482
1439
|
zero-fills omitted fields.
|
|
483
1440
|
- **JSR publish no longer ships `__tests__` helper files** (`casing_codemod.ts`,
|
|
484
|
-
`test_helpers.ts
|
|
1441
|
+
`test_helpers.ts`, including a `node:fs` import subject to JSR type-checking).
|
|
485
1442
|
|
|
486
|
-
### Changed (breaking)
|
|
1443
|
+
### Changed (breaking). Type-level & facade surface
|
|
487
1444
|
|
|
488
1445
|
- **Compile-time typestate across the system, query, relation, and key seams.** The config-form
|
|
489
|
-
`registerSystem` now infers your access declarations as literal types and hands `fn
|
|
490
|
-
`SystemContext<DeclaredAccess<…>>` narrowed to exactly the declared surface
|
|
1446
|
+
`registerSystem` now infers your access declarations as literal types and hands `fn` and `onAdded` a
|
|
1447
|
+
`SystemContext<DeclaredAccess<…>>` narrowed to exactly the declared surface, undeclared access
|
|
491
1448
|
is a compile error naming the missing declaration, with the dev-mode runtime check remaining as
|
|
492
1449
|
the backstop for dynamic values. Query columns are typed by the query's terms
|
|
493
|
-
(`ChunkColumns<Defs>`
|
|
494
|
-
handles carry their cardinality (`RelationDef<"exclusive">` vs `RelationDef<"multi"
|
|
1450
|
+
(`ChunkColumns<Defs>` and `ArchetypeView<Defs>`, `.and(...)` extends the term set), relation
|
|
1451
|
+
handles carry their cardinality (`RelationDef<"exclusive">` vs `RelationDef<"multi">`, the
|
|
495
1452
|
exclusive-only traversal surfaces reject a multi handle at compile time), and
|
|
496
|
-
`ResourceKey
|
|
1453
|
+
`ResourceKey`, `EventKey` and `EventDef` are invariant so a key can no longer widen through
|
|
497
1454
|
`unknown`. A checked-in type battery (`typing_assertions.ts`) pins every rule.
|
|
498
1455
|
- **Grouped facades: `ecs.relations`, `ecs.events`, `ecs.resources`, `ecs.snapshots`.** Cohesive
|
|
499
|
-
secondary surfaces move off the flat namespace onto narrow typed facades
|
|
1456
|
+
secondary surfaces move off the flat namespace onto narrow typed facades:
|
|
500
1457
|
`ecs.relations.add(child, ChildOf, parent)`, `ecs.events.emit(Damage, {...})`,
|
|
501
1458
|
`ecs.resources.get(Time)`, `ecs.snapshots.capture()`. The facades mirror the typestate
|
|
502
1459
|
surface exactly (cardinality-stamped `relations.register`, exclusive-only traversal). Hot-path
|
|
503
|
-
API (component ops, queries, spawn
|
|
504
|
-
are exported type-only
|
|
1460
|
+
API (component ops, queries, spawn and destroy, sparse ops) stays flat by design. Facade classes
|
|
1461
|
+
are exported type-only. The runtime export list is unchanged.
|
|
505
1462
|
- **Value arguments are schema-checked at compile time across every attach seam.** Tag defs
|
|
506
|
-
reject value objects (`Frozen({ x: 1 })` no longer compiles
|
|
1463
|
+
reject value objects (`Frozen({ x: 1 })` no longer compiles, tags carry no data)
|
|
507
1464
|
`addComponents` takes schema-checked entries (`TemplateEntries<Defs>`), so a misspelled or
|
|
508
|
-
cross-component field key is a compile error instead of a silent zero-fill
|
|
1465
|
+
cross-component field key is a compile error instead of a silent zero-fill. Host-seam
|
|
509
1466
|
`queue.spawn` entries (`SpawnEntries<Defs>`) are checked complete against each def's own
|
|
510
|
-
schema (`ValuesArg`
|
|
511
|
-
field list to cover the event schema (`EventFieldsCover`)
|
|
1467
|
+
schema (`ValuesArg` and `CompleteFieldValues` exported), and `events.register` requires the
|
|
1468
|
+
field list to cover the event schema (`EventFieldsCover`), a partial list silently dropped
|
|
512
1469
|
columns and read back `undefined` at runtime. Smaller closures in the same vein: `observe`
|
|
513
1470
|
accepts any `ComponentHandle`, `NoInfer` pins key-typed value params (`events.emit`,
|
|
514
1471
|
resources), and reactive-sync's `JoinReader.field` is constrained to the join's component
|
|
@@ -516,12 +1473,12 @@ rename/removal map:
|
|
|
516
1473
|
|
|
517
1474
|
### Removed (breaking)
|
|
518
1475
|
|
|
519
|
-
- The 29 flat forms the new facades replace (`registerRelation
|
|
520
|
-
`registerEvent
|
|
521
|
-
`removeResource
|
|
522
|
-
`stateHash
|
|
523
|
-
grouped replacement
|
|
524
|
-
`ecs.snapshots.capture()` (was `snapshot()`)
|
|
1476
|
+
- The 29 flat forms the new facades replace (`registerRelation`, `addRelation` and `targetOf`/…,
|
|
1477
|
+
`registerEvent`, `registerSignal`, `emit` and `read`, `registerResource`, `resource` and `setResource`/
|
|
1478
|
+
`removeResource` and `hasResource`, `snapshot`, `restoreInto`, `snapshotSparse` and `restoreSparse`/
|
|
1479
|
+
`stateHash` and `deterministic`, `relationCount` and `compactRelations`). Each maps 1:1 onto its
|
|
1480
|
+
grouped replacement, `ecs.relations.add(...)`, `ecs.events.emit(...)`, `ecs.resources.get(...)`,
|
|
1481
|
+
`ecs.snapshots.capture()` (was `snapshot()`) and `ecs.snapshots.restore(...)` (was
|
|
525
1482
|
`restoreInto(...)`), `ecs.relations.count` (was `relationCount`), `ecs.relations.compact()`
|
|
526
1483
|
(was `compactRelations()`). System-side `ctx.*` and all `Store`-level methods are unchanged.
|
|
527
1484
|
|
|
@@ -529,28 +1486,28 @@ rename/removal map:
|
|
|
529
1486
|
|
|
530
1487
|
- **`Store` decomposed into seven focused collaborators** (RelationService, EventRegistry +
|
|
531
1488
|
ResourceRegistry, EntityAllocator, DeferredCommandBuffer, SnapshotService, ArchetypeGraph) with
|
|
532
|
-
`Store` as the coordinator
|
|
533
|
-
identical
|
|
534
|
-
marker-delimited pass-through band whose logic-free invariant is enforced by an
|
|
1489
|
+
`Store` as the coordinator. Each hot-path extraction was benchmarked against a control that
|
|
1490
|
+
carries identical code, and none regressed. The `ECS` facade's pure delegations now live in a
|
|
1491
|
+
marker-delimited pass-through band whose logic-free invariant is enforced by an ast guard test.
|
|
535
1492
|
- Typed per-consumer host seams (`ObserverHost`, `QueryHost`) replace underscore-convention
|
|
536
|
-
reach-through on `Store
|
|
1493
|
+
reach-through on `Store`. `QueryCache` now owns all 12 query-resolution cache maps.
|
|
537
1494
|
- Store layer consolidation: one strategy-parameterized factory behind
|
|
538
|
-
`growableSabAllocator`
|
|
539
|
-
replaces six structural casts
|
|
1495
|
+
`growableSabAllocator` and `heapArraybufferAllocator`. A typed `isColumnStoreInternal` guard
|
|
1496
|
+
replaces six structural casts. Grow and extend's ~200 duplicated lines moved to a shared
|
|
540
1497
|
`layout_ops.ts` (bit-identical layouts pinned by a golden differential test across the
|
|
541
1498
|
full allocator matrix).
|
|
542
|
-
- `core/reactive` moved to `src/reactive` (the published `./reactive` subpath is unchanged)
|
|
1499
|
+
- `core/reactive` moved to `src/reactive` (the published `./reactive` subpath is unchanged)
|
|
543
1500
|
`__generated__/abi.ts` renamed to `vendored_abi/abi.ts` (it is a hand-maintained snapshot,
|
|
544
1501
|
not generated output).
|
|
545
|
-
- Deleted orphaned duplicate `src/utils/{arrays,constants}.ts
|
|
546
|
-
(shadowed the ECMAScript global) to `AssertionError
|
|
1502
|
+
- Deleted orphaned duplicate `src/utils/{arrays,constants}.ts`. Renamed the custom `TypeError`
|
|
1503
|
+
(shadowed the ECMAScript global) to `AssertionError`. Retired the 246-line casing codemod +
|
|
547
1504
|
guard test (the 0.4 rename has converged).
|
|
548
1505
|
|
|
549
|
-
## [0.4.0]
|
|
1506
|
+
## [0.4.0] - 2026-06-24
|
|
550
1507
|
|
|
551
|
-
Major release. oecs is **re-derived from the upstream oasys engine ECS
|
|
1508
|
+
Major release. oecs is **re-derived from the upstream oasys engine ECS**, its modern descendant, and
|
|
552
1509
|
gains whole subsystems while staying pure-TS and zero-dependency by default. The public API moves to
|
|
553
|
-
the engine's surface, so **every consumer touches breaking changes
|
|
1510
|
+
the engine's surface, so **every consumer touches breaking changes**, chiefly a global
|
|
554
1511
|
`snake_case` → `camelCase` rename. See [docs/MIGRATION-0.3-to-0.4.md](docs/MIGRATION-0.3-to-0.4.md).
|
|
555
1512
|
|
|
556
1513
|
### Changed (breaking)
|
|
@@ -559,127 +1516,127 @@ the engine's surface, so **every consumer touches breaking changes** — chiefly
|
|
|
559
1516
|
from `snake_case` (`create_entity` → `createEntity`, `add_component` → `addComponent`, `get_field` →
|
|
560
1517
|
`getField`, `is_alive` → `isAlive`, `register_system` → `registerSystem`, …). Types and handles stay
|
|
561
1518
|
PascalCase and SCREAMING_SNAKE constants are unchanged. A `vitest` casing guard prevents regressions.
|
|
562
|
-
- **Renamed query
|
|
563
|
-
`query.any_of` → `anyOf
|
|
564
|
-
`event_key
|
|
565
|
-
`is_ecs_error` → `isEcsError
|
|
1519
|
+
- **Renamed query and context verbs.** `QueryBuilder.every` → `with`. `query.not` → `without`
|
|
1520
|
+
`query.any_of` → `anyOf`. `query.for_each` → `forEach`. `archetype.get_column` → `getColumnRead`
|
|
1521
|
+
`event_key`, `signal_key` and `resource_key` → `eventKey`, `signalKey` and `resourceKey`
|
|
1522
|
+
`is_ecs_error` → `isEcsError`. `destroy_entity_deferred` → `destroyEntity` (still deferred).
|
|
566
1523
|
- **Ref mutability flipped on the unsuffixed name.** `ctx.ref` is now the **mutable** default (was
|
|
567
|
-
read-only in 0.3)
|
|
1524
|
+
read-only in 0.3). The read-only variant is `ctx.refRead` (was `ctx.ref_mut` for the mutable one).
|
|
568
1525
|
Same rule for columns: mutable `getColumn` (internal) vs read-only `getColumnRead`.
|
|
569
|
-
- **`WorldOptions` → `ECSOptions
|
|
570
|
-
- **`initial_capacity` removed
|
|
571
|
-
`{ maxBytes }
|
|
1526
|
+
- **`WorldOptions` → `ECSOptions`. `fixed_timestep` → `fixedTimestep`.**
|
|
1527
|
+
- **`initial_capacity` removed**, replaced by the `memory` surface (`memory: { budget }`,
|
|
1528
|
+
`{ maxBytes }`, the `{ columnCapacity }` pin, `{ shared }`, `{ wasm }` and `{ allocator }`). Passing the
|
|
572
1529
|
old option keys throws at construction, pointing at `memory`.
|
|
573
|
-
- **Component-touching systems must declare `reads`
|
|
574
|
-
(tree-shaken from production) validates every column
|
|
1530
|
+
- **Component-touching systems must declare `reads` and `writes`.** A new `__DEV__` access checker
|
|
1531
|
+
(tree-shaken from production) validates every column, ref, field and resource access against a
|
|
575
1532
|
system's declared surface. The bare `(ctx, dt)` and `(q, ctx, dt)` + query-builder `registerSystem`
|
|
576
|
-
overloads declare no access, so a system that touches ECS data through them throws in dev
|
|
1533
|
+
overloads declare no access, so a system that touches ECS data through them throws in dev, move it
|
|
577
1534
|
to the config form (`registerSystem({ reads, writes, fn })`). `exclusive: true` systems bypass the
|
|
578
1535
|
checker. A registration-time lint (`QUERY_ACCESS_UNDECLARED`) additionally checks any declared
|
|
579
1536
|
`queries ⊆ reads ∪ writes`.
|
|
580
|
-
- **`removeComponents` takes an array, not varargs** (`removeComponents(e, [A, B])`)
|
|
581
|
-
`batchAddComponent`
|
|
1537
|
+
- **`removeComponents` takes an array, not varargs** (`removeComponents(e, [A, B])`)
|
|
1538
|
+
`batchAddComponent` and `batchRemoveComponent` key on `ArchetypeID` instead of an `Archetype` object.
|
|
582
1539
|
- **Event schema shape.** `eventKey`'s type parameter is now a field → value-type record
|
|
583
1540
|
(`eventKey<{ target: EntityID; amount: number }>("Damage")`) rather than a tuple of field names, so
|
|
584
|
-
branded fields round-trip through `emit`
|
|
1541
|
+
branded fields round-trip through `emit` and `read`. `registerEvent(key, [...fieldNames])` unchanged
|
|
585
1542
|
otherwise.
|
|
586
1543
|
|
|
587
1544
|
### Added
|
|
588
1545
|
|
|
589
1546
|
- **Two storage profiles over one backing-neutral `ColumnStore`.** Default is pure-TS **heap** (a plain
|
|
590
|
-
resizable `ArrayBuffer`)
|
|
591
|
-
`@oasys/oecs/shared` (`memory: { shared: {} }`) uses a `SharedArrayBuffer` for worker offload
|
|
592
|
-
compute backend. Same code path
|
|
593
|
-
- **Determinism** (opt-in `deterministic: true`): a state hash over column bytes
|
|
594
|
-
`restoreInto()` (and `snapshotSparse`
|
|
595
|
-
shared world with identical history agree. `WorldRestoreError`
|
|
1547
|
+
resizable `ArrayBuffer`), no `SharedArrayBuffer`, no cross-origin isolation. Opt-in
|
|
1548
|
+
`@oasys/oecs/shared` (`memory: { shared: {} }`) uses a `SharedArrayBuffer` for worker offload or a WASM
|
|
1549
|
+
compute backend. Same code path. Identical state hash.
|
|
1550
|
+
- **Determinism** (opt-in `deterministic: true`): a state hash over column bytes plus `snapshot()` and
|
|
1551
|
+
`restoreInto()` (and `snapshotSparse` and `restoreSparse`), **backing-agnostic**, a heap world and a
|
|
1552
|
+
shared world with identical history agree. `WorldRestoreError` and `SparseRestoreError` fail closed
|
|
596
1553
|
before overwriting live backing.
|
|
597
|
-
- **Observers
|
|
1554
|
+
- **Observers**, `world.observe(def, { onAdd, onRemove, onSet, onDisable, onEnable })`, structural +
|
|
598
1555
|
per-entity.
|
|
599
|
-
- **Relations
|
|
600
|
-
`registerIsA`), `(R,*)`
|
|
601
|
-
`ANY_RELATION`), hierarchy queries (`query.hierarchy`), traversal (`ancestorsOf
|
|
1556
|
+
- **Relations**, `(relation, target)` pairs, `ChildOf` and `IsA` presets (`registerChildOf` and
|
|
1557
|
+
`registerIsA`), `(R,*)` and `(*,T)` wildcard queries (`withRelation`, `forEachRelatedTo`,
|
|
1558
|
+
`ANY_RELATION`), hierarchy queries (`query.hierarchy`), traversal (`ancestorsOf`, `rootOf` and
|
|
602
1559
|
`cascadeOf`), and on-delete cleanup policies.
|
|
603
|
-
- **Sparse component storage** (`registerSparseComponent
|
|
604
|
-
**run conditions
|
|
605
|
-
|
|
1560
|
+
- **Sparse component storage** (`registerSparseComponent`, `addSparse` and `query.withSparse`),
|
|
1561
|
+
**run conditions and system sets** (`systemSet` + `configureSet`, `runIfResourceEq`, `runEveryNTicks`
|
|
1562
|
+
and `runIfAnyMatch`), **entity enable and disable** (row-partitioned. `disable`, `enable` and
|
|
606
1563
|
`includeDisabled`), and **templates** (`world.template([...])` + `createEntity(template, overrides)`
|
|
607
|
-
|
|
608
|
-
- **Typed host→ECS write seam
|
|
609
|
-
`HostCommandQueue` drained by a blessed `exclusive` apply system
|
|
610
|
-
(`HostCommandDispatcher`)
|
|
611
|
-
`serializeCommandLog`)
|
|
612
|
-
- **Frame trace
|
|
613
|
-
stream (`__DEV__`-gated). **Compute backend seam
|
|
1564
|
+
and `createEntities(template, count)` for zero-transition spawns).
|
|
1565
|
+
- **Typed host→ECS write seam**, `installHostCommandSeam(world)` + `applyHostCommand` + a
|
|
1566
|
+
`HostCommandQueue` drained by a blessed `exclusive` apply system. A cross-thread ring transport
|
|
1567
|
+
(`HostCommandDispatcher`). Record and replay (`HostCommandRecorder`, `replayCommandLog`,
|
|
1568
|
+
`serializeCommandLog`), and an undo and redo + field-handle layer at `@oasys/oecs/editor`.
|
|
1569
|
+
- **Frame trace**, `world.setTrace(sink)` + `FrameTraceRecorder` emit a structured per-frame event
|
|
1570
|
+
stream (`__DEV__`-gated). **Compute backend seam**, `world.attachBackend(backend)` runs a system's
|
|
614
1571
|
body on a compiled backend instead of its TS closure.
|
|
615
|
-
- **Reactive UI seam (optional):** zero-dependency kernel at `@oasys/oecs/reactive
|
|
616
|
-
bridge at `@oasys/oecs/reactive-sync` (publish-only-dirty, O(changed))
|
|
1572
|
+
- **Reactive UI seam (optional):** zero-dependency kernel at `@oasys/oecs/reactive`. ECS→reactive
|
|
1573
|
+
bridge at `@oasys/oecs/reactive-sync` (publish-only-dirty, O(changed)). SolidJS adapter at
|
|
617
1574
|
`@oasys/oecs/solid` with `solid-js` as an **optional** peer dependency.
|
|
618
|
-
- **`memory` sizing surface** on the constructor: `budget` (by expected `entities`)
|
|
619
|
-
`columnCapacity
|
|
1575
|
+
- **`memory` sizing surface** on the constructor: `budget` (by expected `entities`), `maxBytes`,
|
|
1576
|
+
`columnCapacity`, `shared`, `wasm` and `allocator` arms. `resolveECSMemory(...)` exported to inspect
|
|
620
1577
|
what an intent resolves to.
|
|
621
1578
|
- **Hot-path iteration ergonomics:**
|
|
622
|
-
- **`query.eachChunk((cols, count) => …)
|
|
1579
|
+
- **`query.eachChunk((cols, count) => …)`**, the mutable per-archetype iterator. `cols.mut(def)` and
|
|
623
1580
|
`cols.read(def)` resolve a whole component's field columns at once into a destructurable group
|
|
624
1581
|
(`const { x, y } = cols.mut(Pos)`), stamping the change tick once inside `mut` and handing back
|
|
625
1582
|
`count` (= `entityCount`). The only mutable column accessor reachable through iteration (the
|
|
626
|
-
`ArchetypeView` from `forEach` stays read-only). Honours `includeDisabled()
|
|
627
|
-
- **`ctx.commands
|
|
628
|
-
(`spawn
|
|
1583
|
+
`ArchetypeView` from `forEach` stays read-only). Honours `includeDisabled()`. Dense-only like `forEach`.
|
|
1584
|
+
- **`ctx.commands`**, a Bevy-`Commands`-style facade namespacing the **deferred** structural ops
|
|
1585
|
+
(`spawn`, `add`, `remove`, `despawn`, `disable` and `enable`), unambiguously deferred vs the
|
|
629
1586
|
immediate `world.addComponent`.
|
|
630
|
-
- **Callable bundles
|
|
631
|
-
zero-fill)
|
|
632
|
-
accept a `bundle(...)` or a bare def (tag
|
|
633
|
-
- **`ctx.updateField`
|
|
1587
|
+
- **Callable bundles**, `bundle(def, values)` pairs a def with field values (omitted fields
|
|
1588
|
+
zero-fill). `world.spawnBundle(...)` (immediate) and `ctx.commands.spawn` and `.add` (deferred)
|
|
1589
|
+
accept a `bundle(...)` or a bare def (a tag, or all-zero values), unifying the attach shapes.
|
|
1590
|
+
- **`ctx.updateField` and `ctx.markChanged`**, and optional-component queries (`query.optional(...)` +
|
|
634
1591
|
`getOptionalColumnRead`).
|
|
635
|
-
- **Composable change-detection queries
|
|
636
|
-
mirrors the dense query verbs (`and
|
|
1592
|
+
- **Composable change-detection queries**, `query.changed(...)` returns a `ChangedQuery` that now
|
|
1593
|
+
mirrors the dense query verbs (`and`, `without`, `anyOf` and `optional`), so
|
|
637
1594
|
`q.changed(Pos).without(Dead)` works (refining *after* `changed()`, previously a dead end).
|
|
638
|
-
- **New public exports
|
|
639
|
-
+ `MAX_*` bounds) for snapshot
|
|
640
|
-
`isEcsError`) for catch-and-branch
|
|
1595
|
+
- **New public exports**, entity-ID codec (`createEntityId`, `getEntityIndex` and `getEntityGeneration`
|
|
1596
|
+
+ `MAX_*` bounds) for snapshot and replication decode. The error taxonomy (`ECSError`, `ECS_ERROR`,
|
|
1597
|
+
`isEcsError`) for catch-and-branch, and `@oasys/oecs/primitives` (`BitSet`, `SparseSet`, `SparseMap`,
|
|
641
1598
|
growable typed arrays, `BinaryHeap`, `topologicalSort`).
|
|
642
1599
|
|
|
643
1600
|
### Packaging
|
|
644
1601
|
|
|
645
1602
|
- **Multi-entry build** → `dist/` emits ESM + CJS + `.d.ts` for every subpath (`.`, `/primitives`,
|
|
646
|
-
`/shared`, `/reactive`, `/reactive-sync`, `/editor`, `/solid`)
|
|
647
|
-
keep core consumers from pulling SAB
|
|
1603
|
+
`/shared`, `/reactive`, `/reactive-sync`, `/editor`, `/solid`). `sideEffects:false` + tree-shaking
|
|
1604
|
+
keep core consumers from pulling SAB or Solid. `solid-js` is an optional peer dependency. `jsr.json`
|
|
648
1605
|
exports updated.
|
|
649
1606
|
|
|
650
|
-
## [0.3.3]
|
|
1607
|
+
## [0.3.3] - 2026-04-30
|
|
651
1608
|
|
|
652
1609
|
Release-process and packaging hygiene. No runtime changes.
|
|
653
1610
|
|
|
654
1611
|
### Changed
|
|
655
1612
|
|
|
656
|
-
- **JSR bundle slimmed.** `.github/` and `docs/` are now excluded from the published JSR package. Consumers download less
|
|
1613
|
+
- **JSR bundle slimmed.** `.github/` and `docs/` are now excluded from the published JSR package. Consumers download less. Build and CI artefacts stay on GitHub.
|
|
657
1614
|
- **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.
|
|
658
1615
|
|
|
659
|
-
## [0.3.2]
|
|
1616
|
+
## [0.3.2] - 2026-04-30
|
|
660
1617
|
|
|
661
1618
|
Documentation-only release. No runtime changes.
|
|
662
1619
|
|
|
663
1620
|
### Added
|
|
664
1621
|
|
|
665
1622
|
- **Module overview on `src/index.ts`.** A `@module` block now renders as the JSR Overview tab.
|
|
666
|
-
- **JSDoc on the full public surface.** `ECS` and its public methods, `Query
|
|
1623
|
+
- **JSDoc on the full public surface.** `ECS` and its public methods, `Query`, `QueryBuilder`, `SystemContext` and `ChangedQuery`, all type aliases and interfaces, the event and resource key minters, and the `SCHEDULE` phases are now documented in-source.
|
|
667
1624
|
- **`@internal` tags on internal-but-public TS members** (e.g. `_resolve_query`, `Query._include`, `SystemContext.store`) so JSR hides them from the rendered docs.
|
|
668
1625
|
|
|
669
|
-
## [0.3.1]
|
|
1626
|
+
## [0.3.1] - 2026-04-23
|
|
670
1627
|
|
|
671
|
-
Performance-only patch release. Two targeted allocation-elimination changes on hot paths
|
|
1628
|
+
Performance-only patch release. Two targeted allocation-elimination changes on hot paths. No API changes. Full 466-test suite unchanged.
|
|
672
1629
|
|
|
673
1630
|
### Performance
|
|
674
1631
|
|
|
675
|
-
- **Cache multi-component transition maps on `Archetype`.** `add_components`
|
|
1632
|
+
- **Cache multi-component transition maps on `Archetype`.** `add_components` and `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
1633
|
- **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))
|
|
677
1634
|
|
|
678
|
-
## [0.3.0]
|
|
1635
|
+
## [0.3.0] - 2026-04-21
|
|
679
1636
|
|
|
680
1637
|
A substantial release focused on change detection, stricter component-access
|
|
681
1638
|
typing, and a simpler key-based API for events and resources. Several public
|
|
682
|
-
entry points change shape
|
|
1639
|
+
entry points change shape. See the migration notes under *Breaking changes*.
|
|
683
1640
|
|
|
684
1641
|
### Added
|
|
685
1642
|
|
|
@@ -687,45 +1644,45 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
687
1644
|
|
|
688
1645
|
- Frame-based tick counter on the world. `ECS` now holds a `_tick` that
|
|
689
1646
|
advances once per `update()`. Systems can see it via `ctx.world_tick`,
|
|
690
|
-
and each `SystemContext` receives `last_run_tick
|
|
1647
|
+
and each `SystemContext` receives `last_run_tick`, the tick at which that
|
|
691
1648
|
system last executed.
|
|
692
1649
|
- Per-component change ticks on archetypes. Each archetype tracks
|
|
693
|
-
`_changed_tick[component_id]
|
|
1650
|
+
`_changed_tick[component_id]`, the tick at which any entity in that
|
|
694
1651
|
archetype last had the component mutated. Maintained automatically by
|
|
695
1652
|
`write_fields`, `write_fields_positional`, `copy_shared_from`,
|
|
696
1653
|
`move_entity_from`, and `bulk_move_all_from`, all of which now accept a
|
|
697
1654
|
`tick` parameter.
|
|
698
|
-
- `ChangedQuery<Defs
|
|
1655
|
+
- `ChangedQuery<Defs>`, a new query variant, produced by `query.changed(...)`,
|
|
699
1656
|
that restricts iteration to archetypes whose tracked components were
|
|
700
1657
|
modified after `last_run_tick`. Validates at construction that the named
|
|
701
1658
|
components are part of the parent query's include set.
|
|
702
1659
|
|
|
703
1660
|
#### Readonly component views
|
|
704
1661
|
|
|
705
|
-
- `ReadonlyColumn<T>` and `ReadonlyUint32Array
|
|
1662
|
+
- `ReadonlyColumn<T>` and `ReadonlyUint32Array`, compile-time readonly views
|
|
706
1663
|
of typed-array columns. Returned by `archetype.get_column()` and the new
|
|
707
1664
|
`archetype.entity_ids` getter. Prevents accidental indexed writes at the
|
|
708
|
-
type level
|
|
709
|
-
- `ReadonlyComponentRef<S
|
|
1665
|
+
type level. Zero runtime cost.
|
|
1666
|
+
- `ReadonlyComponentRef<S>`, readonly variant of `ComponentRef`. Returned by
|
|
710
1667
|
`query.ref(...)`. Use it when you only need to read component fields.
|
|
711
|
-
- `archetype.get_column_mut(def, field, tick)
|
|
1668
|
+
- `archetype.get_column_mut(def, field, tick)`, explicit mutable column
|
|
712
1669
|
accessor. Writes through `get_column_mut` update `_changed_tick`.
|
|
713
|
-
- `query.ref_mut(...)
|
|
1670
|
+
- `query.ref_mut(...)`, mutable sibling of `ref()`. Returns a `ComponentRef`
|
|
714
1671
|
and records the component as changed for the current tick.
|
|
715
1672
|
|
|
716
1673
|
#### Key-based Event API
|
|
717
1674
|
|
|
718
|
-
- `EventKey<F
|
|
1675
|
+
- `EventKey<F>`, symbol-typed key that carries the event's field schema as
|
|
719
1676
|
a phantom type.
|
|
720
|
-
- `event_key<F>(name)`
|
|
1677
|
+
- `event_key<F>(name)` and `signal_key(name)`, factories for module-scope
|
|
721
1678
|
event keys. `signal_key` is a convenience wrapper for zero-field events.
|
|
722
1679
|
|
|
723
1680
|
#### Key-based Resource API
|
|
724
1681
|
|
|
725
|
-
- `ResourceKey<T
|
|
1682
|
+
- `ResourceKey<T>`, symbol-typed key carrying the resource's value type as
|
|
726
1683
|
a phantom type.
|
|
727
|
-
- `resource_key<T>(name)
|
|
728
|
-
- `world.has_resource(key)
|
|
1684
|
+
- `resource_key<T>(name)`, factory for module-scope resource keys.
|
|
1685
|
+
- `world.has_resource(key)`, existence check.
|
|
729
1686
|
- Resources are now plain key→value storage. `world.resource(key)` returns
|
|
730
1687
|
the stored `T` directly.
|
|
731
1688
|
|
|
@@ -738,19 +1695,19 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
738
1695
|
|
|
739
1696
|
#### Assertions
|
|
740
1697
|
|
|
741
|
-
- `assert_non_null<T>(value, message?)` in `type_primitives/assertions
|
|
1698
|
+
- `assert_non_null<T>(value, message?)` in `type_primitives/assertions`, a
|
|
742
1699
|
dev-only (`__DEV__` guarded) assertion that narrows `T` to `NonNullable<T>`
|
|
743
1700
|
and throws a `TypeError` with contextual info on failure.
|
|
744
1701
|
|
|
745
1702
|
#### New primitives
|
|
746
1703
|
|
|
747
|
-
- `BinaryHeap<T>` in `type_primitives/binary_heap
|
|
1704
|
+
- `BinaryHeap<T>` in `type_primitives/binary_heap`, generic array-backed
|
|
748
1705
|
heap with a user-supplied comparator. `push`, `pop`, `peek`, `clear`,
|
|
749
|
-
`size`. O(log n) push
|
|
1706
|
+
`size`. O(log n) push and pop, O(1) peek.
|
|
750
1707
|
- `topological_sort<T>(nodes, edges, tiebreaker, node_name?)` in
|
|
751
|
-
`type_primitives/topological_sort
|
|
1708
|
+
`type_primitives/topological_sort`. Kahn's algorithm with a
|
|
752
1709
|
`BinaryHeap`-backed ready queue for deterministic tie-breaking. Throws
|
|
753
|
-
`TypeError` on cycles
|
|
1710
|
+
`TypeError` on cycles. The schedule layer re-wraps as
|
|
754
1711
|
`ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
|
|
755
1712
|
|
|
756
1713
|
#### Public exports
|
|
@@ -764,7 +1721,7 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
764
1721
|
- Query iteration is callback-based. `Query` no longer implements
|
|
765
1722
|
`[Symbol.iterator]`. Iterate with `query.for_each((archetype) => { ... })`.
|
|
766
1723
|
- `world.register_event`, `world.register_signal`, and `world.register_resource`
|
|
767
|
-
return `void` and take an `EventKey`
|
|
1724
|
+
return `void` and take an `EventKey` or a `ResourceKey` as their first argument.
|
|
768
1725
|
- `world.emit`, `world.read`, `world.resource`, and `world.set_resource`
|
|
769
1726
|
accept keys instead of definition objects. `world.resource(key)` returns
|
|
770
1727
|
the typed value `T` directly rather than a field-reader wrapper.
|
|
@@ -772,10 +1729,10 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
772
1729
|
`run_update(label, tick)`, and `run_fixed_update(label, tick)` require
|
|
773
1730
|
the current frame tick. `ECS.update()` wires this automatically.
|
|
774
1731
|
- System ordering now uses the shared `topological_sort` primitive. Observable
|
|
775
|
-
behavior is unchanged: `before
|
|
1732
|
+
behavior is unchanged: `before` and `after` constraints respected,
|
|
776
1733
|
`insertion_order` remains the tie-breaker, cycles surface as
|
|
777
1734
|
`ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
|
|
778
|
-
- Store
|
|
1735
|
+
- Store and query wiring. The store keeps a reference to each active `Query` via
|
|
779
1736
|
`update_query_ref` and calls `mark_non_empty_dirty` only when structural
|
|
780
1737
|
changes occur, avoiding spurious query rebuilds on stable frames.
|
|
781
1738
|
- Bit-manipulation and hash constants (`BITS_PER_WORD`, `BITS_PER_WORD_SHIFT`,
|
|
@@ -795,10 +1752,10 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
795
1752
|
### Removed
|
|
796
1753
|
|
|
797
1754
|
- `ResourceChannel`, `ResourceDef<F>`, `ResourceReader<F>`, `ResourceID`,
|
|
798
|
-
`as_resource_id`, and the `__resource_schema` marker symbol
|
|
1755
|
+
`as_resource_id`, and the `__resource_schema` marker symbol, the entire
|
|
799
1756
|
SoA column-based resource storage layer. Resources are now key→value.
|
|
800
|
-
- `RESOURCE_ROW` constant
|
|
801
|
-
- `EventDef<F
|
|
1757
|
+
- `RESOURCE_ROW` constant, unused.
|
|
1758
|
+
- `EventDef<F>`, replaced by `EventKey<F>`.
|
|
802
1759
|
|
|
803
1760
|
### Breaking changes
|
|
804
1761
|
|
|
@@ -814,7 +1771,7 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
814
1771
|
world.emit(DAMAGE, { amount: 5 });
|
|
815
1772
|
```
|
|
816
1773
|
|
|
817
|
-
2. **Resource registration
|
|
1774
|
+
2. **Resource registration and access.**
|
|
818
1775
|
```ts
|
|
819
1776
|
// before
|
|
820
1777
|
const clock = world.register_resource({ ms: "u32" } as const, { ms: 0 });
|
|
@@ -825,7 +1782,7 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
825
1782
|
world.register_resource(CLOCK, { ms: 0 });
|
|
826
1783
|
const ms = world.resource(CLOCK).ms;
|
|
827
1784
|
```
|
|
828
|
-
`world.resource()` returns the stored value directly
|
|
1785
|
+
`world.resource()` returns the stored value directly. The reader wrapper
|
|
829
1786
|
and the SoA column storage are gone.
|
|
830
1787
|
|
|
831
1788
|
3. **Query iteration.**
|
|
@@ -838,7 +1795,7 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
838
1795
|
```
|
|
839
1796
|
|
|
840
1797
|
4. **Mutable vs readonly refs.** `query.ref(...)` now returns
|
|
841
|
-
`ReadonlyComponentRef`. Switch to `query.ref_mut(...)` when
|
|
1798
|
+
`ReadonlyComponentRef`. Switch to `query.ref_mut(...)` when you write, because
|
|
842
1799
|
this is also what enables change detection for that component.
|
|
843
1800
|
|
|
844
1801
|
5. **Archetype column access.** `archetype.get_column(...)` returns a
|
|
@@ -851,4 +1808,4 @@ entry points change shape; see the migration notes under *Breaking changes*.
|
|
|
851
1808
|
|
|
852
1809
|
## [0.2.1] and earlier
|
|
853
1810
|
|
|
854
|
-
Prior releases
|
|
1811
|
+
Prior releases, see git history.
|