@forgeax/engine-ecs 0.1.26 → 0.1.28
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/README.md +94 -35
- package/dist/__tests__/world-read.unit.test.d.ts +2 -0
- package/dist/__tests__/world-read.unit.test.d.ts.map +1 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/index.mjs +3471 -4263
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +2 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +3 -333
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.mjs.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/world-entity-lifecycle.d.ts +3 -14
- package/dist/world-entity-lifecycle.d.ts.map +1 -1
- package/dist/world-internal.d.ts +65 -5
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world-read.d.ts +16 -0
- package/dist/world-read.d.ts.map +1 -0
- package/dist/world-read.mjs +8 -0
- package/dist/world-read.mjs.map +1 -0
- package/dist/world-scheduling.d.ts +0 -4
- package/dist/world-scheduling.d.ts.map +1 -1
- package/dist/world-storage-primitives.d.ts +26 -0
- package/dist/world-storage-primitives.d.ts.map +1 -0
- package/dist/world.d.ts +352 -157
- package/dist/world.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/__tests__/command-buffer.test.ts +29 -3
- package/src/__tests__/hierarchy.unit.test.ts +3 -3
- package/src/__tests__/world-health.contract.test.ts +195 -2
- package/src/__tests__/world-read.unit.test.ts +30 -0
- package/src/commands.ts +22 -9
- package/src/internal.ts +5 -3
- package/src/world-entity-lifecycle.ts +23 -252
- package/src/world-internal-augmentation.d.ts +11 -0
- package/src/world-internal.ts +114 -63
- package/src/world-read.ts +38 -0
- package/src/world-scheduling.ts +0 -26
- package/src/world-storage-primitives.ts +179 -0
- package/src/world.ts +2590 -509
- package/dist/world-component-access.d.ts +0 -311
- package/dist/world-component-access.d.ts.map +0 -1
- package/dist/world-component-storage.d.ts +0 -298
- package/dist/world-component-storage.d.ts.map +0 -1
- package/dist/world-core.d.ts +0 -39
- package/dist/world-core.d.ts.map +0 -1
- package/src/world-component-access.ts +0 -1769
- package/src/world-component-storage.ts +0 -1264
- package/src/world-core.ts +0 -74
package/README.md
CHANGED
|
@@ -10,6 +10,11 @@ mutation, two schedules, resources, time, and optional shared numeric kernels.
|
|
|
10
10
|
> in their owning packages. Do not add a second ECS facade for one of those
|
|
11
11
|
> domains.
|
|
12
12
|
|
|
13
|
+
`World` directly owns its graph, entity records, managed stores, relationship
|
|
14
|
+
indexes, epochs, structural evidence, and execution health. Query, command, and
|
|
15
|
+
lifecycle helpers receive only the typed package-internal capabilities they
|
|
16
|
+
actually consume; there is no `WorldCore`, `WorldData`, or second state bag.
|
|
17
|
+
|
|
13
18
|
```mermaid
|
|
14
19
|
flowchart LR
|
|
15
20
|
HOST["App host"] --> WORLD["World.update(delta)"]
|
|
@@ -169,50 +174,18 @@ relationship source: adding `ChildOf` also materializes its required
|
|
|
169
174
|
components. The reverse `Children` projection does not gain a second write
|
|
170
175
|
path, and no frame system scans the world to repair the dependency.
|
|
171
176
|
|
|
172
|
-
An exclusive relationship can be moved through the generic owner method. Using
|
|
173
|
-
the schema above, pass the source component and its declared source-field data:
|
|
174
|
-
`world.reparent(child, newParent, ChildOf, { parent: newParent })`.
|
|
175
|
-
For another relationship, replace `ChildOf` and `parent` with that schema's
|
|
176
|
-
source component and source field; both are required. `linkedSpawn: true`
|
|
177
|
-
means despawning a target recursively despawns its linked source entities;
|
|
178
|
-
`linkedSpawn: false` leaves those entities alive and their source edge is then
|
|
179
|
-
an explicit dangling state that the owning domain must repair.
|
|
180
|
-
|
|
181
177
|
`Children` and `AnimationTargets` are read projections, not a second write
|
|
182
|
-
authority. `Children { entities
|
|
178
|
+
authority. `Children { entities` is a materialized target owned by ECS; the
|
|
183
179
|
Scene package owns the `ChildOf` vocabulary and chooses where to use it. The
|
|
184
180
|
same rule applies to `AnimationTargets`. Direct target writes are rejected by
|
|
185
181
|
`World` at both the type and runtime boundaries.
|
|
186
182
|
|
|
187
|
-
Every writable relationship source uses that same owner path: `World.set`, row
|
|
188
|
-
mutation, structural add/re-add/reparent, spawn, and deferred Commands all
|
|
189
|
-
enter the owner for liveness/cycle validation and target preparation. Expected
|
|
190
|
-
relationship rejection leaves the source, mirror, backpointer, versions, and
|
|
191
|
-
epochs unchanged. Query spans reject both relationship roles before exposing a
|
|
192
|
-
raw column, because a numeric source write cannot maintain the reverse
|
|
193
|
-
projection. `query.spans()` and the internal derived-writer seam report
|
|
194
|
-
`query-span-unavailable` with `detail.reason === 'relationship-component'` for
|
|
195
|
-
any writable relationship source or target; use `World.set`, `row.mut`, or a
|
|
196
|
-
structural command so the owner can converge both sides.
|
|
197
|
-
|
|
198
|
-
Deferred `Commands.spawn` uses the same component-data shape as `World.spawn`:
|
|
199
|
-
each entry is `{ component, data }`, and the pending handle is materialized at
|
|
200
|
-
command flush. Relationship validation and mirror preparation happen before
|
|
201
|
-
the committed source row; expected failures leave the World healthy, while an
|
|
202
|
-
unknown post-write failure poisons it and requires an App-owned rebuild.
|
|
203
|
-
|
|
204
183
|
## Queries and projections
|
|
205
184
|
|
|
206
185
|
Queries are the only public data-plane API. A row is the flexible path; a span
|
|
207
186
|
is the packed numeric path and includes entity handles for owner-side identity.
|
|
208
187
|
Raw `Table`, `Archetype`, `Column`, and `FieldView` values are package-private.
|
|
209
188
|
|
|
210
|
-
Use `query.spans()` only for dense, table-only descriptors. It rejects optional
|
|
211
|
-
data and sparse components with `query-span-unavailable`; `changed` and `added`
|
|
212
|
-
filters are supported and yield only matching contiguous runs. Span columns
|
|
213
|
-
are zero-copy transient TypedArray views and are invalid after a structure
|
|
214
|
-
epoch change.
|
|
215
|
-
|
|
216
189
|
```ts
|
|
217
190
|
const query = world.query({ read: [Position] }).unwrap();
|
|
218
191
|
for (const row of query) console.log(row.entity, row.get(Position).x);
|
|
@@ -238,6 +211,65 @@ for (const span of changed.spans().unwrap()) {
|
|
|
238
211
|
// the owner's ordinary Query and then drain the changed Query once.
|
|
239
212
|
```
|
|
240
213
|
|
|
214
|
+
`added` is the matching first-observation filter for component membership. It
|
|
215
|
+
uses the same row-version cursor as `changed`, so each consumer drains its own
|
|
216
|
+
query independently:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const added = world.query({ read: [Position], added: [Position] }).unwrap();
|
|
220
|
+
for (const row of added) initializeProjection(row.entity, row.get(Position));
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Structural membership facts have a bounded, typed cursor in the projection
|
|
224
|
+
subpath. Read after the last cursor, consume every returned event, and rebuild
|
|
225
|
+
from an ordinary query when the ring reports overflow:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { readStructuralEvidence } from '@forgeax/engine-ecs/projection';
|
|
229
|
+
|
|
230
|
+
let cursor = 0;
|
|
231
|
+
const evidence = readStructuralEvidence(world, cursor);
|
|
232
|
+
if (evidence.status === 'overflow') {
|
|
233
|
+
rebuildProjectionFromQuery(world);
|
|
234
|
+
cursor = evidence.cursor;
|
|
235
|
+
} else {
|
|
236
|
+
for (const event of evidence.events) applyStructuralEvent(event);
|
|
237
|
+
cursor = evidence.cursor;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The `@forgeax/engine-ecs/world-read` seam is a read-only owner capability for
|
|
242
|
+
hot semantic scalar/array probes. `World.getStructureEpoch()` remains the
|
|
243
|
+
public cache-invalidation primitive; the seam never returns tables, archetypes,
|
|
244
|
+
columns, or mutable views. Ordinary gameplay code should continue to use
|
|
245
|
+
`World.get` and queries.
|
|
246
|
+
|
|
247
|
+
When an owner needs one scalar or one array element without materializing a row,
|
|
248
|
+
import the capability explicitly and keep the probe read-only. Entity fields
|
|
249
|
+
are returned as their stored u32, so narrow a non-null value to the existing
|
|
250
|
+
`EntityHandle` before passing it to another entity-typed probe:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { ENTITY_NULL_RAW, type EntityHandle } from '@forgeax/engine-ecs';
|
|
254
|
+
import { ChildOf, Children } from '@forgeax/engine-scene';
|
|
255
|
+
import { worldRead } from '@forgeax/engine-ecs/world-read';
|
|
256
|
+
|
|
257
|
+
const parentRaw = world[worldRead].getFieldValue(child, ChildOf, 'parent');
|
|
258
|
+
const parent: EntityHandle | undefined =
|
|
259
|
+
parentRaw === undefined || parentRaw === ENTITY_NULL_RAW
|
|
260
|
+
? undefined
|
|
261
|
+
: (parentRaw as EntityHandle);
|
|
262
|
+
const count =
|
|
263
|
+
parent === undefined ? undefined : world[worldRead].getArrayLength(parent, Children, 'entities');
|
|
264
|
+
const firstChild =
|
|
265
|
+
parent === undefined
|
|
266
|
+
? undefined
|
|
267
|
+
: world[worldRead].getArrayElement(parent, Children, 'entities', 0);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`undefined` means the entity, component, field, or element is unavailable; the
|
|
271
|
+
capability never hands out a storage view and cannot mutate the World.
|
|
272
|
+
|
|
241
273
|
Queries never expose table ids, rows, columns, or a duplicate snapshot data
|
|
242
274
|
plane.
|
|
243
275
|
|
|
@@ -282,6 +314,28 @@ if (!result.ok) {
|
|
|
282
314
|
}
|
|
283
315
|
```
|
|
284
316
|
|
|
317
|
+
Recovery means constructing a fresh World and replaying the authoritative
|
|
318
|
+
game state; a poisoned identity is never reused:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
function createWorld(): World {
|
|
322
|
+
const next = new World();
|
|
323
|
+
next.addSystem(Update, Move).unwrap();
|
|
324
|
+
return next;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let liveWorld = createWorld();
|
|
328
|
+
const step = liveWorld.update(1 / 60);
|
|
329
|
+
if (!step.ok && liveWorld.execution.health === 'poisoned') {
|
|
330
|
+
// The first failed frame can be `system-failed`; a later call is
|
|
331
|
+
// `world-poisoned`. Health is the stable recovery boundary for both.
|
|
332
|
+
liveWorld = createWorld();
|
|
333
|
+
for (const saved of savedPositions) {
|
|
334
|
+
liveWorld.spawn({ component: Position, data: saved }).unwrap();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
285
339
|
Expected command failures are reported before structural commit and leave the
|
|
286
340
|
World unchanged. A system throw or an unknown post-write failure cannot prove
|
|
287
341
|
that no row was mutated: the World becomes poisoned and must be rebuilt by the
|
|
@@ -314,6 +368,7 @@ their owner instead of being forwarded through the root.
|
|
|
314
368
|
| `@forgeax/engine-ecs` | World, components, relationships, queries, schedules, resources, errors |
|
|
315
369
|
| `@forgeax/engine-ecs/projection` | Explicit render read versions, typed structural evidence, and numeric spans |
|
|
316
370
|
| `@forgeax/engine-ecs/shared` | Shared numeric kernel contracts |
|
|
371
|
+
| `@forgeax/engine-ecs/world-read` | Safe semantic scalar/array reads for owner-package hot paths |
|
|
317
372
|
| `@forgeax/engine-ecs/externalization` | Generic component projection and entity remap |
|
|
318
373
|
|
|
319
374
|
`Result`, `ok`, `err`, and `Handle` come from `@forgeax/engine-types`; ECS does
|
|
@@ -331,7 +386,11 @@ consumer needs one of those concerns, move the owner to App, Scene, Render,
|
|
|
331
386
|
Physics, or the explicitly named ECS subpath.
|
|
332
387
|
</details>
|
|
333
388
|
|
|
334
|
-
##
|
|
389
|
+
## Contract invariants
|
|
390
|
+
|
|
391
|
+
The following are package-contract statements, not a claim that every
|
|
392
|
+
repository-wide browser, Dawn, or consumer gate is green. Gate results for a
|
|
393
|
+
specific change belong to its closed-loop verification report.
|
|
335
394
|
|
|
336
395
|
- [x] Entity and component mutation use one World authority.
|
|
337
396
|
- [x] Relationship targets remain materialized for $O(1 + k)$ reads.
|
|
@@ -341,7 +400,7 @@ Physics, or the explicitly named ECS subpath.
|
|
|
341
400
|
- [x] Advanced projection/shared/externalization APIs are named subpaths.
|
|
342
401
|
|
|
343
402
|
For the full migration rationale and acceptance matrix, see the canonical
|
|
344
|
-
[ECS
|
|
403
|
+
[ECS World ownership simplification design](../../.forgeax-harness/docs/specs/2026-09-08-ecs-worldcore-simplification-design.md).
|
|
345
404
|
|
|
346
405
|
### Large managed arrays
|
|
347
406
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world-read.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/world-read.unit.test.ts"],"names":[],"mappings":""}
|
package/dist/commands.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Result } from '@forgeax/engine-types';
|
|
|
2
2
|
import type { Component, ComponentSchema } from './component';
|
|
3
3
|
import { type EntityHandle } from './entity-handle';
|
|
4
4
|
import { type CommandKind } from './errors';
|
|
5
|
+
import type { WorldExecutionState } from './execution/shared-kernel';
|
|
5
6
|
import type { ComponentData, EcsError } from './world';
|
|
6
7
|
import { type WorldInternal, worldInternal } from './world-internal';
|
|
7
8
|
export type Command = {
|
|
@@ -42,6 +43,7 @@ export interface CommandBuffer {
|
|
|
42
43
|
/** Minimal world interface needed by CommandBuffer for entity allocation. */
|
|
43
44
|
export interface WorldForCommands {
|
|
44
45
|
readonly [worldInternal]: WorldInternal;
|
|
46
|
+
readonly execution: WorldExecutionState;
|
|
45
47
|
/**
|
|
46
48
|
* Allocate a pending entity index, returning [entity handle, index slot].
|
|
47
49
|
* @internal
|
package/dist/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI9D,OAAO,EAAmB,KAAK,YAAY,EAAiC,MAAM,iBAAiB,CAAC;AACpG,OAAO,EAGL,KAAK,WAAW,EAQjB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
|