@forgeax/engine-ecs 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +35 -18
  2. package/dist/__tests__/query-idle.unit.test.d.ts +2 -0
  3. package/dist/__tests__/query-idle.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/set-allocation.unit.test.d.ts +2 -0
  5. package/dist/__tests__/set-allocation.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/state-projection.unit.test.d.ts +2 -0
  7. package/dist/__tests__/state-projection.unit.test.d.ts.map +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.mjs +579 -623
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/projection/index.d.ts +2 -18
  13. package/dist/projection/index.d.ts.map +1 -1
  14. package/dist/projection/index.mjs +433 -23
  15. package/dist/projection/index.mjs.map +1 -1
  16. package/dist/projection/state-projection.d.ts +37 -0
  17. package/dist/projection/state-projection.d.ts.map +1 -0
  18. package/dist/query/query.d.ts.map +1 -1
  19. package/dist/shared-ref-store.d.ts +0 -24
  20. package/dist/shared-ref-store.d.ts.map +1 -1
  21. package/dist/shared.mjs.map +1 -1
  22. package/dist/storage/archetype-graph.d.ts +2 -0
  23. package/dist/storage/archetype-graph.d.ts.map +1 -1
  24. package/dist/storage/change-detection.d.ts +4 -0
  25. package/dist/storage/change-detection.d.ts.map +1 -1
  26. package/dist/storage/table.d.ts +6 -3
  27. package/dist/storage/table.d.ts.map +1 -1
  28. package/dist/world-internal.d.ts +0 -2
  29. package/dist/world-internal.d.ts.map +1 -1
  30. package/dist/world.d.ts +0 -2
  31. package/dist/world.d.ts.map +1 -1
  32. package/package.json +4 -4
  33. package/src/__tests__/component-version-surface.test.ts +1 -7
  34. package/src/__tests__/derived-range-writer.contract.test.ts +6 -0
  35. package/src/__tests__/ecs-core-reduction.characterization.test.ts +2 -3
  36. package/src/__tests__/execution-conflict-boundary.unit.test.ts +4 -0
  37. package/src/__tests__/externalization-render-read-lease.unit.test.ts +4 -10
  38. package/src/__tests__/query-idle.unit.test.ts +19 -0
  39. package/src/__tests__/set-allocation.unit.test.ts +30 -0
  40. package/src/__tests__/shared-ref-lifetime.unit.test.ts +1 -2
  41. package/src/__tests__/shared-ref-store.unit.test.ts +2 -34
  42. package/src/__tests__/state-projection.unit.test.ts +159 -0
  43. package/src/__tests__/world-health.contract.test.ts +0 -23
  44. package/src/index.ts +1 -1
  45. package/src/projection/index.ts +9 -39
  46. package/src/projection/state-projection.ts +250 -0
  47. package/src/query/query.ts +23 -16
  48. package/src/shared-ref-store.ts +0 -70
  49. package/src/storage/archetype-graph.ts +15 -1
  50. package/src/storage/change-detection.ts +36 -4
  51. package/src/storage/table.ts +20 -1
  52. package/src/world-internal.ts +0 -2
  53. package/src/world.ts +35 -38
  54. package/dist/__tests__/structural-evidence.contract.test-d.d.ts +0 -2
  55. package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +0 -1
  56. package/dist/__tests__/structural-evidence.contract.test.d.ts +0 -2
  57. package/dist/__tests__/structural-evidence.contract.test.d.ts.map +0 -1
  58. package/dist/storage/structural-evidence.d.ts +0 -30
  59. package/dist/storage/structural-evidence.d.ts.map +0 -1
  60. package/src/__tests__/structural-evidence.contract.test-d.ts +0 -6
  61. package/src/__tests__/structural-evidence.contract.test.ts +0 -49
  62. package/src/storage/structural-evidence.ts +0 -64
@@ -29,6 +29,8 @@ export interface ArchetypeGraph {
29
29
  /** Global generation counter. Incremented on each new archetype. */
30
30
  generation: number;
31
31
  tables: Table[];
32
+ activeTables: Set<Table>;
33
+ activeTablesByComponent: Map<ComponentId, Set<Table>>;
32
34
  tableDedupByKey: Map<string, TableId>;
33
35
  tableGeneration: number;
34
36
  sparseTags: Map<ComponentId, SparseTagSet>;
@@ -44,6 +46,8 @@ export function createArchetypeGraph(shared = false): ArchetypeGraph {
44
46
  dedupByKey: new Map(),
45
47
  generation: 0,
46
48
  tables: [],
49
+ activeTables: new Set(),
50
+ activeTablesByComponent: new Map(),
47
51
  tableDedupByKey: new Map(),
48
52
  tableGeneration: 0,
49
53
  sparseTags: new Map(),
@@ -67,7 +71,17 @@ export function getOrCreateTable(
67
71
  const key = tableKey(tableComponents.map((component) => componentOwner.componentId(component)));
68
72
  const existingId = graph.tableDedupByKey.get(key);
69
73
  if (existingId !== undefined) return getTable(graph, existingId);
70
- const table = createTable(tableComponents, graph.tables.length, graph.shared);
74
+ const directories = [graph.activeTables];
75
+ for (const component of tableComponents) {
76
+ const id = componentOwner.componentId(component);
77
+ let directory = graph.activeTablesByComponent.get(id);
78
+ if (directory === undefined) {
79
+ directory = new Set();
80
+ graph.activeTablesByComponent.set(id, directory);
81
+ }
82
+ directories.push(directory);
83
+ }
84
+ const table = createTable(tableComponents, graph.tables.length, graph.shared, directories);
71
85
  graph.tables.push(table);
72
86
  graph.tableDedupByKey.set(key, table.id);
73
87
  graph.tableGeneration += 1;
@@ -7,15 +7,22 @@ import { type EntityHandle, entityIndex } from '../entity-handle';
7
7
  import type { ArchetypeGraph } from './archetype-graph';
8
8
  import { getOrCreateSparseTagSet } from './archetype-graph';
9
9
 
10
+ export const PROJECTION_BLOCK_SIZE = 256;
11
+
10
12
  const INITIAL_SPARSE_CAPACITY = 64;
11
13
 
12
14
  export interface ComponentEpochColumns {
13
15
  added: Float64Array;
14
16
  changed: Float64Array;
17
+ blocks: Float64Array;
15
18
  }
16
19
 
17
20
  export function createComponentEpochColumns(capacity: number): ComponentEpochColumns {
18
- return { added: new Float64Array(capacity), changed: new Float64Array(capacity) };
21
+ return {
22
+ added: new Float64Array(capacity),
23
+ changed: new Float64Array(capacity),
24
+ blocks: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE)),
25
+ };
19
26
  }
20
27
 
21
28
  export function growComponentEpochColumns(
@@ -26,7 +33,9 @@ export function growComponentEpochColumns(
26
33
  const changed = new Float64Array(capacity);
27
34
  added.set(columns.added);
28
35
  changed.set(columns.changed);
29
- return { added, changed };
36
+ const blocks = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
37
+ blocks.set(columns.blocks);
38
+ return { added, changed, blocks };
30
39
  }
31
40
 
32
41
  export function copyComponentEpoch(
@@ -37,6 +46,8 @@ export function copyComponentEpoch(
37
46
  ): void {
38
47
  target.added[targetRow] = source.added[sourceRow] ?? 0;
39
48
  target.changed[targetRow] = source.changed[sourceRow] ?? 0;
49
+ const block = Math.floor(targetRow / PROJECTION_BLOCK_SIZE);
50
+ target.blocks[block] = Math.max(target.blocks[block] ?? 0, target.changed[targetRow] ?? 0);
40
51
  }
41
52
 
42
53
  export interface SparseTagSet {
@@ -190,7 +201,7 @@ export function markComponentsAdded(
190
201
  const epochs = table?.storage.get(componentId)?.epochs;
191
202
  if (epochs === undefined) continue;
192
203
  epochs.added[tableRow] = epoch;
193
- epochs.changed[tableRow] = epoch;
204
+ publishComponentRange(epochs, tableRow, 1, epoch);
194
205
  }
195
206
  }
196
207
 
@@ -212,5 +223,26 @@ export function markComponentChanged(
212
223
  const epochs = graph.tables[archetype.tableId]?.storage.get(componentId)?.epochs;
213
224
  if (epochs === undefined) return;
214
225
  const tableRow = archetype.rows[location.archetypeRow] ?? -1;
215
- epochs.changed[tableRow] = epoch();
226
+ publishComponentRange(epochs, tableRow, 1, epoch());
227
+ }
228
+
229
+ /** Row evidence and its conservative block summary share the World epoch. */
230
+ export function publishComponentRange(
231
+ columns: ComponentEpochColumns,
232
+ start: number,
233
+ count: number,
234
+ epoch: number,
235
+ ): void {
236
+ if (count === 0) return;
237
+ if (count === 1) {
238
+ columns.changed[start] = epoch;
239
+ columns.blocks[Math.floor(start / PROJECTION_BLOCK_SIZE)] = epoch;
240
+ return;
241
+ }
242
+ columns.changed.fill(epoch, start, start + count);
243
+ columns.blocks.fill(
244
+ epoch,
245
+ Math.floor(start / PROJECTION_BLOCK_SIZE),
246
+ Math.ceil((start + count) / PROJECTION_BLOCK_SIZE),
247
+ );
216
248
  }
@@ -17,6 +17,7 @@ import {
17
17
  copyComponentEpoch,
18
18
  createComponentEpochColumns,
19
19
  growComponentEpochColumns,
20
+ PROJECTION_BLOCK_SIZE,
20
21
  } from './change-detection';
21
22
  import { arrayCountColumnName, type Column, createColumn, growColumn } from './column';
22
23
 
@@ -38,6 +39,8 @@ export interface Table {
38
39
  size: number;
39
40
  capacity: number;
40
41
  version: number;
42
+ membership: Float64Array;
43
+ readonly activeDirectories: readonly Set<Table>[];
41
44
  }
42
45
 
43
46
  export function tableKey(componentIds: ReadonlyArray<ComponentId>): string {
@@ -55,6 +58,7 @@ export function createTable(
55
58
  components: ReadonlyArray<Component>,
56
59
  id: TableId,
57
60
  shared = false,
61
+ activeDirectories: readonly Set<Table>[] = [new Set<Table>()],
58
62
  ): Table {
59
63
  const sortedComponents = canonicalComponents(components);
60
64
  const capacity = INITIAL_CAPACITY;
@@ -102,23 +106,31 @@ export function createTable(
102
106
  size: 0,
103
107
  capacity,
104
108
  version: 0,
109
+ membership: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE)),
110
+ activeDirectories,
105
111
  };
106
112
  }
107
113
 
108
- export function appendTableRow(table: Table, entity: EntityHandle): number {
114
+ export function appendTableRow(table: Table, entity: EntityHandle, epoch = 0): number {
109
115
  if (table.size === table.capacity) growTable(table, table.capacity * 2);
110
116
  const row = table.size;
111
117
  const self = table.storage.get(componentId(Entity))?.fields.get('self');
112
118
  if (self !== undefined) self.view[row] = entity as number;
113
119
  table.size = row + 1;
120
+ if (row === 0) for (const directory of table.activeDirectories) directory.add(table);
121
+ markTableMembership(table, row, epoch);
114
122
  return row;
115
123
  }
116
124
 
117
125
  export function removeTableRow(
118
126
  table: Table,
119
127
  row: number,
128
+ epoch = 0,
120
129
  ): { movedEntity: EntityHandle; newRow: number } | null {
121
130
  const lastRow = table.size - 1;
131
+ markTableMembership(table, row, epoch);
132
+ markTableMembership(table, lastRow, epoch);
133
+ if (lastRow === 0) for (const directory of table.activeDirectories) directory.delete(table);
122
134
  if (row === lastRow) {
123
135
  table.size = lastRow;
124
136
  return null;
@@ -148,6 +160,13 @@ export function growTable(table: Table, targetCapacity: number): void {
148
160
  componentStorage.fields = fields;
149
161
  componentStorage.epochs = growComponentEpochColumns(componentStorage.epochs, capacity);
150
162
  }
163
+ const membership = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
164
+ membership.set(table.membership);
165
+ table.membership = membership;
151
166
  table.capacity = capacity;
152
167
  table.version += 1;
153
168
  }
169
+
170
+ export function markTableMembership(table: Table, row: number, epoch: number): void {
171
+ table.membership[Math.floor(row / PROJECTION_BLOCK_SIZE)] = epoch;
172
+ }
@@ -23,7 +23,6 @@ import type { SharedRefStore } from './shared-ref-store';
23
23
  import type { Archetype } from './storage/archetype';
24
24
  import type { ArchetypeGraph } from './storage/archetype-graph';
25
25
  import type { ChangeTicks } from './storage/change-detection';
26
- import type { StructuralEvidenceRing } from './storage/structural-evidence';
27
26
  import type { Table } from './storage/table';
28
27
  import type { ClockWriter } from './time';
29
28
  import type { ComponentData, EcsError, EntityRecord } from './world';
@@ -75,7 +74,6 @@ export interface WorldInternal {
75
74
  readonly getSchedules: () => ReadonlyMap<ScheduleToken, Schedule>;
76
75
  readonly getSharedRefs: () => SharedRefStore;
77
76
  readonly getStructureEpoch: () => number;
78
- readonly getStructuralEvidence: () => StructuralEvidenceRing;
79
77
  readonly lookupAlive: (
80
78
  entity: EntityHandle,
81
79
  operation: string,
package/src/world.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { publishComponentRange } from './storage/change-detection';
1
2
  // @forgeax/engine-ecs — World: top-level ECS container.
2
3
  //
3
4
  // World owns entities, archetypes (via ArchetypeGraph), and component registry.
@@ -131,9 +132,7 @@ import {
131
132
  type FieldView,
132
133
  normalizeBufferWrite,
133
134
  } from './storage/column';
134
- import type { StructuralEvidenceInput } from './storage/structural-evidence';
135
- import { StructuralEvidenceRing as StructuralEvidenceRingImpl } from './storage/structural-evidence';
136
- import { appendTableRow, removeTableRow, type Table } from './storage/table';
135
+ import { appendTableRow, markTableMembership, removeTableRow, type Table } from './storage/table';
137
136
  import {
138
137
  createWorldClock,
139
138
  DEFAULT_TIME_POLICY,
@@ -373,7 +372,6 @@ export class World {
373
372
  /** Per-World shared-ref store; public read-only for direct handle operations. */
374
373
  readonly sharedRefs: SharedRefStore = new SharedRefStoreImpl();
375
374
  private readonly componentMutationEpochs: number[] = [];
376
- private readonly structuralEvidence = new StructuralEvidenceRingImpl();
377
375
  /** One packed reverse index per relationship source component. */
378
376
  private readonly relationshipIndexes = new Map<number, RelationshipIndex>();
379
377
  private mutationEpoch = 0;
@@ -430,7 +428,6 @@ export class World {
430
428
  getSchedules: () => this.schedules,
431
429
  getSharedRefs: () => this.sharedRefs,
432
430
  getStructureEpoch: this.getStructureEpoch.bind(this),
433
- getStructuralEvidence: () => this.structuralEvidence,
434
431
  lookupAlive: this.lookupAlive.bind(this),
435
432
  markComponentChanged: this.internalmarkComponentChanged.bind(this),
436
433
  markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
@@ -512,10 +509,6 @@ export class World {
512
509
  return worldScheduleUsesComponent(this, component);
513
510
  }
514
511
 
515
- private recordStructuralEvidence(evidence: StructuralEvidenceInput): void {
516
- this.structuralEvidence.append(evidence);
517
- }
518
-
519
512
  /** Resolve current logical identity for a packed entity handle. */
520
513
  private internalgetEntityArchetype(entity: EntityHandle): Archetype | undefined {
521
514
  const record = this.records[entityIndex(entity)];
@@ -557,7 +550,7 @@ export class World {
557
550
  if (epochs === undefined) {
558
551
  throw new Error(`Derived component ${componentId} is not in the table.`);
559
552
  }
560
- epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
553
+ publishComponentRange(epochs, rowStart, rowCount, epoch);
561
554
  this.componentMutationEpochs[componentId] = epoch;
562
555
  }
563
556
 
@@ -606,7 +599,7 @@ export class World {
606
599
  const epochs = table.storage.get(componentId)?.epochs;
607
600
  if (epochs === undefined || rowCount === 0) return;
608
601
  const epoch = this.internalnextMutationEpoch();
609
- epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
602
+ publishComponentRange(epochs, rowStart, rowCount, epoch);
610
603
  this.componentMutationEpochs[componentId] = epoch;
611
604
  }
612
605
 
@@ -1410,11 +1403,6 @@ export class World {
1410
1403
  if (mirrorAdded) {
1411
1404
  this.internalmarkComponentsAdded(target, [mirrorLocalId]);
1412
1405
  this.advanceStructureEpoch();
1413
- this.recordStructuralEvidence({
1414
- kind: 'component-added',
1415
- entity: target,
1416
- componentId: mirrorLocalId,
1417
- });
1418
1406
  }
1419
1407
  return ok(undefined);
1420
1408
  }
@@ -1651,12 +1639,8 @@ export class World {
1651
1639
  data: value,
1652
1640
  });
1653
1641
  if (!valuePreflight.ok) return valuePreflight;
1654
- const currentValue = this.readRow(arch, component, row) as Record<string, unknown>;
1655
- const mergedValue = {
1656
- ...currentValue,
1657
- ...(value as Record<string, unknown>),
1658
- };
1659
- const enumError = validateEnumFieldValues(component, mergedValue, entity as number);
1642
+ // Enum constraints are field-local; untouched fields are already validated.
1643
+ const enumError = validateEnumFieldValues(component, value, entity as number);
1660
1644
  if (enumError !== null) return err(enumError as unknown as EcsError);
1661
1645
 
1662
1646
  if (component.storage === 'sparse') {
@@ -2191,11 +2175,6 @@ export class World {
2191
2175
  // components-added epoch/evidence record that claims the edge exists.
2192
2176
  this.internalmarkComponentsAdded(entity, [componentId(componentData.component)]);
2193
2177
  this.advanceStructureEpoch();
2194
- this.recordStructuralEvidence({
2195
- kind: 'component-added',
2196
- entity,
2197
- componentId: localId,
2198
- });
2199
2178
  }
2200
2179
  return ok(undefined);
2201
2180
  }
@@ -2308,12 +2287,8 @@ export class World {
2308
2287
  this.migrateEntity(rec, srcArch, targetArch);
2309
2288
  }
2310
2289
  if (!internal) {
2290
+ this.internalnextMutationEpoch();
2311
2291
  this.advanceStructureEpoch();
2312
- this.recordStructuralEvidence({
2313
- kind: 'component-removed',
2314
- entity,
2315
- componentId: localId,
2316
- });
2317
2292
  }
2318
2293
  return ok(undefined);
2319
2294
  }
@@ -2378,7 +2353,7 @@ export class World {
2378
2353
  record.archetypeId = arch.id;
2379
2354
  storageTouched = true;
2380
2355
  const table = this.table(arch);
2381
- const tableRow = appendTableRow(table, entity);
2356
+ const tableRow = appendTableRow(table, entity, this.mutationEpoch + 1);
2382
2357
  const archetypeRow = appendArchetypeRow(arch, tableRow);
2383
2358
  record.archetypeRow = archetypeRow;
2384
2359
 
@@ -2415,7 +2390,6 @@ export class World {
2415
2390
  ...componentDatas.map((cd) => componentId(cd.component)),
2416
2391
  ]);
2417
2392
  this.advanceStructureEpoch();
2418
- this.recordStructuralEvidence({ kind: 'spawn', entity });
2419
2393
  return ok(undefined);
2420
2394
  } catch (error) {
2421
2395
  if (storageTouched) this.poisonAfterEntityMutation('World.materializeEntity', error);
@@ -3021,6 +2995,28 @@ export class World {
3021
2995
  const isVariable = arrayMeta.length === undefined;
3022
2996
  const fixedLength = arrayMeta.length ?? 0;
3023
2997
 
2998
+ // Same-typed inline storage can copy elements without packing temporary bytes.
2999
+ // TypedArray.set preserves overlapping source views; different element types
3000
+ // retain the byte-copy contract below.
3001
+ if (
3002
+ !isVariable &&
3003
+ metaKey !== 'shared' &&
3004
+ meta.viewCtor !== undefined &&
3005
+ col.view instanceof meta.viewCtor &&
3006
+ (raw == null || Array.isArray(raw) || raw instanceof meta.viewCtor)
3007
+ ) {
3008
+ const start = row * col.arity;
3009
+ const count = raw == null ? 0 : Math.min(raw.length, col.arity);
3010
+ if (Array.isArray(raw)) {
3011
+ for (let i = 0; i < count; i++)
3012
+ col.view[start + i] = typeof raw[i] === 'number' ? raw[i] : 0;
3013
+ } else if (raw != null) {
3014
+ col.view.set(raw.length <= col.arity ? raw : raw.subarray(0, count), start);
3015
+ }
3016
+ col.view.fill(0, start + count, start + col.arity);
3017
+ return;
3018
+ }
3019
+
3024
3020
  // Determine the payload's logical element count. Accept any TypedArray
3025
3021
  // (Float32Array / Uint32Array / etc.) plus plain numeric arrays; an
3026
3022
  // undefined / missing payload is treated as a length-0 init. Bytes are
@@ -3230,7 +3226,7 @@ export class World {
3230
3226
  const entity = (srcTable.storage.get(componentId(EntityComponent))?.fields.get('self')?.view[
3231
3227
  oldTableRow
3232
3228
  ] ?? 0) as EntityHandle;
3233
- const newTableRow = appendTableRow(targetTable, entity);
3229
+ const newTableRow = appendTableRow(targetTable, entity, this.mutationEpoch + 1);
3234
3230
  const newArchetypeRow = appendArchetypeRow(targetArch, newTableRow);
3235
3231
 
3236
3232
  // Copy shared component data.
@@ -3276,7 +3272,7 @@ export class World {
3276
3272
  movedRecord.archetypeRow = archetypeSwap.newRow;
3277
3273
  }
3278
3274
  }
3279
- const tableSwap = removeTableRow(srcTable, oldTableRow);
3275
+ const tableSwap = removeTableRow(srcTable, oldTableRow, this.mutationEpoch + 1);
3280
3276
  if (tableSwap !== null) {
3281
3277
  const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
3282
3278
  if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
@@ -3313,6 +3309,7 @@ export class World {
3313
3309
  }
3314
3310
  record.archetypeId = targetArch.id;
3315
3311
  record.archetypeRow = appendArchetypeRow(targetArch, tableRow);
3312
+ markTableMembership(table, tableRow, this.mutationEpoch + 1);
3316
3313
  }
3317
3314
 
3318
3315
  /**
@@ -3366,7 +3363,7 @@ export class World {
3366
3363
  movedRecord.archetypeRow = archetypeSwap.newRow;
3367
3364
  }
3368
3365
  }
3369
- const tableSwap = removeTableRow(table, tableRow);
3366
+ const tableSwap = removeTableRow(table, tableRow, this.mutationEpoch + 1);
3370
3367
  if (tableSwap !== null) {
3371
3368
  const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
3372
3369
  if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
@@ -3377,7 +3374,6 @@ export class World {
3377
3374
  }
3378
3375
  }
3379
3376
  }
3380
- this.recordStructuralEvidence({ kind: 'despawn', entity });
3381
3377
  record.archetypeId = -1;
3382
3378
  record.archetypeRow = -1;
3383
3379
  record.generation += 1;
@@ -3386,6 +3382,7 @@ export class World {
3386
3382
  const childResult = this.despawnEntity(child, true);
3387
3383
  if (!childResult.ok) return childResult;
3388
3384
  }
3385
+ this.internalnextMutationEpoch();
3389
3386
  this.advanceStructureEpoch();
3390
3387
  return ok(undefined);
3391
3388
  } catch (error) {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=structural-evidence.contract.test-d.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structural-evidence.contract.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/structural-evidence.contract.test-d.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=structural-evidence.contract.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structural-evidence.contract.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/structural-evidence.contract.test.ts"],"names":[],"mappings":""}
@@ -1,30 +0,0 @@
1
- import type { EntityHandle } from '../entity-handle';
2
- export type StructuralEvidenceKind = 'spawn' | 'despawn' | 'component-added' | 'component-removed';
3
- export interface StructuralEvidenceInput {
4
- readonly kind: StructuralEvidenceKind;
5
- readonly entity: EntityHandle;
6
- readonly componentId?: number;
7
- }
8
- export interface StructuralEvidence extends StructuralEvidenceInput {
9
- readonly sequence: number;
10
- }
11
- export type StructuralEvidenceRead = {
12
- readonly status: 'ok';
13
- readonly cursor: number;
14
- readonly events: readonly StructuralEvidence[];
15
- } | {
16
- readonly status: 'overflow';
17
- readonly cursor: number;
18
- readonly oldestAvailable: number;
19
- };
20
- /** Bounded producer-owned structural evidence; consumers never infer facts by scanning. */
21
- export declare class StructuralEvidenceRing {
22
- readonly capacity: number;
23
- private readonly events;
24
- private nextSequence;
25
- constructor(capacity?: number);
26
- get cursor(): number;
27
- append(input: StructuralEvidenceInput): number;
28
- readAfter(cursor: number): StructuralEvidenceRead;
29
- }
30
- //# sourceMappingURL=structural-evidence.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structural-evidence.d.ts","sourceRoot":"","sources":["../../src/storage/structural-evidence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,SAAS,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAEnG,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,sBAAsB,GAC9B;IACE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAChD,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F,2FAA2F;AAC3F,qBAAa,sBAAsB;IAIrB,QAAQ,CAAC,QAAQ;IAH7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,YAAY,CAAK;gBAEJ,QAAQ,SAAO;IAOpC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM;IAO9C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB;CAkBlD"}
@@ -1,6 +0,0 @@
1
- import type { StructuralEvidence, StructuralEvidenceKind } from '../storage/structural-evidence';
2
-
3
- const kind: StructuralEvidenceKind = 'component-added';
4
- const evidence: StructuralEvidence = { sequence: 1, kind, entity: 0 as never, componentId: 4 };
5
- // @ts-expect-error evidence sequence is producer-owned.
6
- evidence.sequence = 2;
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { encodeEntity } from '../entity-handle';
3
- import { StructuralEvidenceRing } from '../storage/structural-evidence';
4
-
5
- describe('typed structural evidence ring', () => {
6
- it('records lifecycle, component membership, packed identity, and sequence facts', () => {
7
- const ring = new StructuralEvidenceRing(8);
8
- const entity = encodeEntity(4, 2);
9
- const cursor = ring.cursor;
10
- ring.append({ kind: 'spawn', entity });
11
- ring.append({ kind: 'component-added', entity, componentId: 7 });
12
- ring.append({ kind: 'component-removed', entity, componentId: 7 });
13
- ring.append({ kind: 'despawn', entity });
14
- const read = ring.readAfter(cursor);
15
- expect(read.status).toBe('ok');
16
- if (read.status !== 'ok') return;
17
- expect(
18
- read.events.map(({ kind, entity: packed, sequence }) => ({ kind, packed, sequence })),
19
- ).toEqual([
20
- { kind: 'spawn', packed: entity, sequence: 1 },
21
- { kind: 'component-added', packed: entity, sequence: 2 },
22
- { kind: 'component-removed', packed: entity, sequence: 3 },
23
- { kind: 'despawn', packed: entity, sequence: 4 },
24
- ]);
25
- });
26
-
27
- it('fails closed on overflow and exposes the cursor for consumer reconciliation', () => {
28
- const ring = new StructuralEvidenceRing(2);
29
- const cursor = ring.cursor;
30
- const entity = encodeEntity(1, 4);
31
- const reused = encodeEntity(1, 5);
32
- ring.append({ kind: 'spawn', entity });
33
- ring.append({ kind: 'despawn', entity });
34
- ring.append({ kind: 'spawn', entity: reused });
35
- expect(ring.readAfter(cursor)).toMatchObject({
36
- status: 'overflow',
37
- cursor: 3,
38
- oldestAvailable: 2,
39
- });
40
- expect(ring.cursor).toBe(3);
41
- });
42
-
43
- it('rejects cursors outside the producer-owned sequence range', () => {
44
- const ring = new StructuralEvidenceRing(4);
45
- const entity = encodeEntity(3, 1);
46
- ring.append({ kind: 'spawn', entity });
47
- expect(() => ring.readAfter(2)).toThrow(RangeError);
48
- });
49
- });
@@ -1,64 +0,0 @@
1
- import type { EntityHandle } from '../entity-handle';
2
-
3
- export type StructuralEvidenceKind = 'spawn' | 'despawn' | 'component-added' | 'component-removed';
4
-
5
- export interface StructuralEvidenceInput {
6
- readonly kind: StructuralEvidenceKind;
7
- readonly entity: EntityHandle;
8
- readonly componentId?: number;
9
- }
10
-
11
- export interface StructuralEvidence extends StructuralEvidenceInput {
12
- readonly sequence: number;
13
- }
14
-
15
- export type StructuralEvidenceRead =
16
- | {
17
- readonly status: 'ok';
18
- readonly cursor: number;
19
- readonly events: readonly StructuralEvidence[];
20
- }
21
- | { readonly status: 'overflow'; readonly cursor: number; readonly oldestAvailable: number };
22
-
23
- /** Bounded producer-owned structural evidence; consumers never infer facts by scanning. */
24
- export class StructuralEvidenceRing {
25
- private readonly events: Array<StructuralEvidence | undefined>;
26
- private nextSequence = 1;
27
-
28
- constructor(readonly capacity = 1024) {
29
- if (!Number.isSafeInteger(capacity) || capacity <= 0) {
30
- throw new RangeError('StructuralEvidenceRing capacity must be a positive safe integer');
31
- }
32
- this.events = new Array<StructuralEvidence | undefined>(capacity);
33
- }
34
-
35
- get cursor(): number {
36
- return this.nextSequence - 1;
37
- }
38
-
39
- append(input: StructuralEvidenceInput): number {
40
- const sequence = this.nextSequence;
41
- this.nextSequence += 1;
42
- this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
43
- return sequence;
44
- }
45
-
46
- readAfter(cursor: number): StructuralEvidenceRead {
47
- const latest = this.cursor;
48
- if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
49
- throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
50
- }
51
- const oldestAvailable = Math.max(1, latest - this.capacity + 1);
52
- if (cursor < oldestAvailable - 1)
53
- return { status: 'overflow', cursor: latest, oldestAvailable };
54
- const events: StructuralEvidence[] = [];
55
- for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
56
- const event = this.events[(sequence - 1) % this.capacity];
57
- if (event === undefined || event.sequence !== sequence) {
58
- return { status: 'overflow', cursor: latest, oldestAvailable };
59
- }
60
- events.push(event);
61
- }
62
- return { status: 'ok', cursor: latest, events };
63
- }
64
- }