archetype-ecs 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +79 -14
  2. package/bench/allocations-1m.js +1 -1
  3. package/bench/component-churn-bench.js +193 -0
  4. package/bench/iterate.wat +95 -0
  5. package/bench/multi-ecs-bench.js +1 -1
  6. package/bench/run-js-vs-go-ts.sh +147 -0
  7. package/bench/typed-vs-bitecs-1m.js +1 -1
  8. package/bench/typed-vs-untyped.js +1 -1
  9. package/bench/vs-bitecs.js +1 -1
  10. package/bench/wasm-iteration-bench.js +289 -0
  11. package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
  12. package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
  13. package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
  14. package/dist/{src/System.d.ts → System.d.ts} +4 -0
  15. package/dist/{src/System.js → System.js} +25 -5
  16. package/dist/WasmArena.d.ts +13 -0
  17. package/dist/WasmArena.js +48 -0
  18. package/dist/{src/index.d.ts → index.d.ts} +7 -9
  19. package/dist/{src/index.js → index.js} +2 -0
  20. package/dist/wasm-kernels.d.ts +10 -0
  21. package/dist/wasm-kernels.js +59 -0
  22. package/package.json +12 -7
  23. package/src/ComponentRegistry.ts +7 -3
  24. package/src/EntityManager.ts +209 -119
  25. package/src/System.ts +34 -9
  26. package/src/WasmArena.ts +83 -0
  27. package/src/index.ts +16 -11
  28. package/src/iterate.wat +135 -0
  29. package/src/wasm-kernels.ts +68 -0
  30. package/tests/EntityManager.test.ts +51 -86
  31. package/tests/System.test.ts +184 -0
  32. package/tests/types.ts +1 -1
  33. package/tsconfig.json +2 -2
  34. package/tsconfig.test.json +13 -0
  35. package/dist/tests/EntityManager.test.d.ts +0 -1
  36. package/dist/tests/EntityManager.test.js +0 -651
  37. package/dist/tests/System.test.d.ts +0 -1
  38. package/dist/tests/System.test.js +0 -630
  39. package/dist/tests/types.d.ts +0 -1
  40. package/dist/tests/types.js +0 -129
  41. /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
  42. /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
  43. /package/dist/{src/Profiler.js → Profiler.js} +0 -0
@@ -1,11 +1,13 @@
1
- import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec } from './ComponentRegistry.js';
1
+ import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec, type FieldRef } from './ComponentRegistry.js';
2
+ import { WasmArena, type NumericTypedArrayConstructor } from './WasmArena.js';
3
+ import { instantiateKernelsSync, isWasmSimdAvailable, type IterateKernels } from './wasm-kernels.js';
4
+
5
+ export type { FieldRef } from './ComponentRegistry.js';
2
6
 
3
7
  export type EntityId = number;
4
8
 
5
- export interface FieldRef {
6
- readonly _sym: symbol;
7
- readonly _field: string;
8
- }
9
+ export type SoAArrayValue = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array
10
+ | Uint8Array | Uint16Array | Uint32Array | unknown[];
9
11
 
10
12
  export interface ArchetypeView {
11
13
  readonly id: number;
@@ -15,6 +17,8 @@ export interface ArchetypeView {
15
17
  readonly snapshotCount: number;
16
18
  field(ref: FieldRef): any;
17
19
  fieldStride(ref: FieldRef): number;
20
+ fieldOffset(ref: FieldRef): number;
21
+ fieldAdd(target: FieldRef, source: FieldRef): void;
18
22
  snapshot(ref: FieldRef): any;
19
23
  }
20
24
 
@@ -24,14 +28,16 @@ export interface SerializedData {
24
28
  components: Record<string, Record<string, unknown>>;
25
29
  }
26
30
 
31
+ export type ComponentData = Record<string, number | string | ArrayLike<number>> | null | undefined;
32
+
27
33
  export interface EntityManager {
28
34
  createEntity(): EntityId;
29
35
  destroyEntity(id: EntityId): void;
30
- addComponent(entityId: EntityId, type: ComponentDef, data?: any): void;
36
+ addComponent(entityId: EntityId, type: ComponentDef, data?: ComponentData): void;
31
37
  removeComponent(entityId: EntityId, type: ComponentDef): void;
32
- getComponent(entityId: EntityId, type: ComponentDef): any;
33
- get(entityId: EntityId, fieldRef: FieldRef): any;
34
- set(entityId: EntityId, fieldRef: FieldRef, value: any): void;
38
+ getComponent(entityId: EntityId, type: ComponentDef): Record<string, number | string | number[]> | undefined;
39
+ get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined;
40
+ set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void;
35
41
  hasComponent(entityId: EntityId, type: ComponentDef): boolean;
36
42
  query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
37
43
  getAllEntities(): EntityId[];
@@ -41,6 +47,7 @@ export interface EntityManager {
41
47
  onAdd(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
42
48
  onRemove(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
43
49
  flushHooks(): void;
50
+ commitRemovals(): void;
44
51
  enableTracking(filterComponent: ComponentDef): void;
45
52
  flushChanges(): { created: Set<EntityId>; destroyed: Set<EntityId> };
46
53
  flushSnapshots(): void;
@@ -55,6 +62,7 @@ export interface EntityManager {
55
62
  nameToSymbol: Record<string, ComponentDef>,
56
63
  options?: { deserializers?: Map<string, (data: unknown) => unknown> }
57
64
  ): void;
65
+ readonly wasmMemory: WebAssembly.Memory | null;
58
66
  }
59
67
 
60
68
  // ── Internal types ───────────────────────────────────────
@@ -64,10 +72,10 @@ interface SoAStore {
64
72
  _schema: Record<string, TypeSpec>;
65
73
  _capacity: number;
66
74
  _arraySizes: Record<string, number>;
67
- [field: string]: any;
75
+ _fields: Record<string, SoAArrayValue>;
68
76
  }
69
77
 
70
- type SnapshotStore = Record<string, any>;
78
+ type SnapshotStore = Record<string, SoAArrayValue>;
71
79
 
72
80
  interface Archetype {
73
81
  key: Uint32Array;
@@ -151,78 +159,93 @@ function maskKey(mask: Uint32Array): string {
151
159
 
152
160
  // ── SoA helpers ──────────────────────────────────────────
153
161
 
154
- function unpackSpec(spec: TypeSpec): [any, number] {
162
+ function unpackSpec(spec: TypeSpec): [{ new(len: number): SoAArrayValue }, number] {
155
163
  if (Array.isArray(spec)) return spec;
156
164
  return [spec, 0];
157
165
  }
158
166
 
159
- function createSoAStore(schema: Record<string, TypeSpec>, capacity: number): SoAStore {
160
- const store: any = { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: {} };
167
+ function createSoAStore(schema: Record<string, TypeSpec>, capacity: number, arena?: WasmArena): SoAStore {
168
+ const fields: Record<string, SoAArrayValue> = {};
169
+ const arraySizes: Record<string, number> = {};
161
170
  for (const [field, spec] of Object.entries(schema)) {
162
171
  const [Ctor, size] = unpackSpec(spec);
163
- if (size > 0) {
164
- store[field] = new Ctor(capacity * size);
165
- store._arraySizes[field] = size;
172
+ const count = size > 0 ? capacity * size : capacity;
173
+ if (size > 0) arraySizes[field] = size;
174
+ if (arena && Ctor !== Array) {
175
+ const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
176
+ const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
177
+ arena.createView(NumCtor, offset, count, fields, field);
166
178
  } else {
167
- store[field] = new Ctor(capacity);
179
+ fields[field] = new Ctor(count);
168
180
  }
169
181
  }
170
- return store;
182
+ return { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: arraySizes, _fields: fields };
171
183
  }
172
184
 
173
- function growSoAStore(store: SoAStore, newCapacity: number): void {
185
+ function growSoAStore(store: SoAStore, newCapacity: number, arena?: WasmArena): void {
174
186
  store._capacity = newCapacity;
175
187
  for (const [field, spec] of Object.entries(store._schema)) {
176
188
  const [Ctor, size] = unpackSpec(spec);
177
- const old = store[field];
189
+ const old = store._fields[field];
178
190
  const allocSize = size > 0 ? newCapacity * size : newCapacity;
179
- store[field] = new Ctor(allocSize);
180
- if (Ctor === Array) {
181
- for (let i = 0; i < old.length; i++) store[field][i] = old[i];
191
+ if (arena && Ctor !== Array) {
192
+ const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
193
+ const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
194
+ arena.updateView(store._fields, field, offset, NumCtor, allocSize);
195
+ (store._fields[field] as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
182
196
  } else {
183
- store[field].set(old);
197
+ const grown = new Ctor(allocSize);
198
+ if (Ctor === Array) {
199
+ for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
200
+ } else {
201
+ (grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
202
+ }
203
+ store._fields[field] = grown;
184
204
  }
185
205
  }
186
206
  }
187
207
 
188
- function soaWrite(store: SoAStore, idx: number, data: any): void {
208
+ function soaWrite(store: SoAStore, idx: number, data: ComponentData): void {
189
209
  if (!data) {
190
210
  for (const field in store._schema) {
211
+ const arr = store._fields[field];
191
212
  const size = store._arraySizes[field] || 0;
192
213
  if (size > 0) {
193
214
  const base = idx * size;
194
- for (let j = 0; j < size; j++) store[field][base + j] = 0;
215
+ for (let j = 0; j < size; j++) (arr as never[])[base + j] = 0 as never;
195
216
  } else {
196
- store[field][idx] = 0;
217
+ (arr as never[])[idx] = 0 as never;
197
218
  }
198
219
  }
199
220
  return;
200
221
  }
201
222
  for (const field in store._schema) {
223
+ const arr = store._fields[field];
202
224
  const size = store._arraySizes[field] || 0;
203
225
  if (size > 0) {
204
226
  const base = idx * size;
205
227
  const src = data[field];
206
228
  if (src) {
207
229
  for (let j = 0; j < size; j++) {
208
- store[field][base + j] = src[j] ?? 0;
230
+ (arr as never[])[base + j] = ((src as ArrayLike<number>)[j] ?? 0) as never;
209
231
  }
210
232
  }
211
233
  } else {
212
- store[field][idx] = data[field];
234
+ (arr as never[])[idx] = data[field] as never;
213
235
  }
214
236
  }
215
237
  }
216
238
 
217
- function soaRead(store: SoAStore, idx: number): Record<string, any> {
218
- const obj: Record<string, any> = {};
239
+ function soaRead(store: SoAStore, idx: number): Record<string, number | string | number[]> {
240
+ const obj: Record<string, number | string | number[]> = {};
219
241
  for (const field in store._schema) {
242
+ const arr = store._fields[field];
220
243
  const size = store._arraySizes[field] || 0;
221
244
  if (size > 0) {
222
245
  const base = idx * size;
223
- obj[field] = Array.from(store[field].subarray(base, base + size));
246
+ obj[field] = Array.from((arr as Float32Array).subarray(base, base + size));
224
247
  } else {
225
- obj[field] = store[field][idx];
248
+ obj[field] = (arr as never[])[idx];
226
249
  }
227
250
  }
228
251
  return obj;
@@ -230,7 +253,7 @@ function soaRead(store: SoAStore, idx: number): Record<string, any> {
230
253
 
231
254
  function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
232
255
  for (const field in store._schema) {
233
- const arr = store[field];
256
+ const arr = store._fields[field] as never[];
234
257
  const size = store._arraySizes[field] || 0;
235
258
  if (size > 0) {
236
259
  const baseA = idxA * size;
@@ -248,36 +271,51 @@ function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
248
271
  }
249
272
  }
250
273
 
251
- function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number): SnapshotStore {
274
+ function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number, arena?: WasmArena): SnapshotStore {
252
275
  const snap: SnapshotStore = {};
253
276
  for (const [field, spec] of Object.entries(schema)) {
254
277
  const [Ctor, size] = unpackSpec(spec);
255
- snap[field] = new Ctor(size > 0 ? capacity * size : capacity);
278
+ const count = size > 0 ? capacity * size : capacity;
279
+ if (arena && Ctor !== Array) {
280
+ const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
281
+ const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
282
+ arena.createView(NumCtor, offset, count, snap, field);
283
+ } else {
284
+ snap[field] = new Ctor(count);
285
+ }
256
286
  }
257
287
  return snap;
258
288
  }
259
289
 
260
- function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number): void {
290
+ function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number, arena?: WasmArena): void {
261
291
  for (const [field, spec] of Object.entries(schema)) {
262
292
  const [Ctor, size] = unpackSpec(spec);
263
293
  const old = snap[field];
264
- snap[field] = new Ctor(size > 0 ? newCapacity * size : newCapacity);
265
- if (Ctor === Array) {
266
- for (let i = 0; i < old.length; i++) snap[field][i] = old[i];
294
+ const allocSize = size > 0 ? newCapacity * size : newCapacity;
295
+ if (arena && Ctor !== Array) {
296
+ const NumCtor = Ctor as unknown as NumericTypedArrayConstructor;
297
+ const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
298
+ arena.updateView(snap, field, offset, NumCtor, allocSize);
299
+ (snap[field] as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
267
300
  } else {
268
- snap[field].set(old);
301
+ const grown = new Ctor(allocSize);
302
+ if (Ctor === Array) {
303
+ for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
304
+ } else {
305
+ (grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
306
+ }
307
+ snap[field] = grown;
269
308
  }
270
309
  }
271
310
  }
272
311
 
273
312
  // ── Entity Manager ───────────────────────────────────────
274
313
 
275
- type DeferredOp =
276
- | { kind: 'add'; entityId: EntityId; comp: ComponentDef; data?: any }
277
- | { kind: 'remove'; entityId: EntityId; comp: ComponentDef }
278
- | { kind: 'destroy'; entityId: EntityId };
314
+ export function createEntityManager(options?: { wasm?: boolean }): EntityManager {
315
+ const useWasm = options?.wasm ?? isWasmSimdAvailable();
316
+ const arena = useWasm ? new WasmArena() : undefined;
317
+ const kernels: IterateKernels | null = arena ? instantiateKernelsSync(arena.memory) : null;
279
318
 
280
- export function createEntityManager(): EntityManager {
281
319
  let nextId: EntityId = 1;
282
320
  let nextArchId = 1;
283
321
  const allEntityIds = new Set<EntityId>();
@@ -289,7 +327,13 @@ export function createEntityManager(): EntityManager {
289
327
 
290
328
  let hooks: Hooks | null = null;
291
329
 
330
+ const removedData = new Map<EntityId, Map<symbol, Record<string, number | string | number[]>>>();
331
+
292
332
  // Deferred structural changes during forEach iteration
333
+ type DeferredOp =
334
+ | { kind: 'add'; entityId: EntityId; comp: ComponentDef; data?: ComponentData }
335
+ | { kind: 'remove'; entityId: EntityId; comp: ComponentDef }
336
+ | { kind: 'destroy'; entityId: EntityId };
293
337
  let iterating = 0;
294
338
  const deferred: DeferredOp[] = [];
295
339
 
@@ -342,10 +386,10 @@ export function createEntityManager(): EntityManager {
342
386
  };
343
387
  for (const t of types) {
344
388
  const schema = componentSchemas.get(t);
345
- const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
389
+ const store = schema ? createSoAStore(schema, INITIAL_CAPACITY, arena) : null;
346
390
  arch.components.set(t, store);
347
391
  if (tracked && store) {
348
- arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY));
392
+ arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY, arena));
349
393
  }
350
394
  }
351
395
  archetypes.set(key, arch);
@@ -361,22 +405,22 @@ export function createEntityManager(): EntityManager {
361
405
  arch.capacity = newCap;
362
406
  for (const [type, store] of arch.components) {
363
407
  if (store) {
364
- growSoAStore(store, newCap);
408
+ growSoAStore(store, newCap, arena);
365
409
  if (arch.snapshots) {
366
410
  const snap = arch.snapshots.get(type);
367
- if (snap) growSnapshotStore(snap, store._schema, newCap);
411
+ if (snap) growSnapshotStore(snap, store._schema, newCap, arena);
368
412
  }
369
413
  }
370
414
  }
371
415
  }
372
416
 
373
- function addToArchetype(arch: Archetype, entityId: EntityId, componentMap: Record<symbol, any>): void {
417
+ function addToArchetype(arch: Archetype, entityId: EntityId, componentMap: Map<symbol, ComponentData>): void {
374
418
  ensureCapacity(arch);
375
419
  const idx = arch.count;
376
420
  arch.entityIds[idx] = entityId;
377
421
  for (const t of arch.types) {
378
422
  const store = arch.components.get(t);
379
- if (store) soaWrite(store, idx, (componentMap as any)[t]);
423
+ if (store) soaWrite(store, idx, componentMap.get(t));
380
424
  }
381
425
  arch.entityToIndex.set(entityId, idx);
382
426
  arch.count++;
@@ -402,7 +446,7 @@ export function createEntityManager(): EntityManager {
402
446
  entityArchetype.delete(entityId);
403
447
  }
404
448
 
405
- function readComponentData(arch: Archetype, type: symbol, idx: number): any {
449
+ function readComponentData(arch: Archetype, type: symbol, idx: number): Record<string, number | string | number[]> | undefined {
406
450
  const store = arch.components.get(type);
407
451
  if (!store) return undefined;
408
452
  return soaRead(store, idx);
@@ -430,8 +474,6 @@ export function createEntityManager(): EntityManager {
430
474
  return matching;
431
475
  }
432
476
 
433
- // ── Internal structural change implementations ──────────
434
-
435
477
  function doDestroyEntity(id: EntityId): void {
436
478
  const arch = entityArchetype.get(id);
437
479
  if (arch) {
@@ -440,6 +482,19 @@ export function createEntityManager(): EntityManager {
440
482
  const pending = hooks.pendingRemove.get(type);
441
483
  if (pending) pending.push(id);
442
484
  }
485
+ if (hooks.removeCbs.size > 0) {
486
+ const idx = arch.entityToIndex.get(id)!;
487
+ let entitySnap: Map<symbol, Record<string, number | string | number[]>> | undefined;
488
+ for (const type of arch.types) {
489
+ if (hooks.removeCbs.has(type)) {
490
+ const store = arch.components.get(type);
491
+ if (store) {
492
+ if (!entitySnap) { entitySnap = new Map(); removedData.set(id, entitySnap); }
493
+ entitySnap.set(type, soaRead(store, idx));
494
+ }
495
+ }
496
+ }
497
+ }
443
498
  }
444
499
  if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(id);
445
500
  removeFromArchetype(arch, id);
@@ -447,13 +502,13 @@ export function createEntityManager(): EntityManager {
447
502
  allEntityIds.delete(id);
448
503
  }
449
504
 
450
- function doAddComponent(entityId: EntityId, comp: ComponentDef, data?: any): void {
505
+ function doAddComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
451
506
  const type = toSym(comp);
452
507
  const arch = entityArchetype.get(entityId);
453
508
 
454
509
  if (!arch) {
455
510
  const newArch = getOrCreateArchetype([type]);
456
- addToArchetype(newArch, entityId, { [type as unknown as string]: data });
511
+ addToArchetype(newArch, entityId, new Map([[type, data]]));
457
512
  if (hooks) {
458
513
  const pending = hooks.pendingAdd.get(type);
459
514
  if (pending) pending.push(entityId);
@@ -474,9 +529,9 @@ export function createEntityManager(): EntityManager {
474
529
  const newArch = getOrCreateArchetype(newTypes);
475
530
 
476
531
  const idx = arch.entityToIndex.get(entityId)!;
477
- const map: any = { [type as unknown as string]: data };
532
+ const map = new Map<symbol, ComponentData>([[type, data]]);
478
533
  for (const t of arch.types) {
479
- map[t as unknown as string] = readComponentData(arch, t, idx);
534
+ map.set(t, readComponentData(arch, t, idx));
480
535
  }
481
536
 
482
537
  removeFromArchetype(arch, entityId);
@@ -495,6 +550,14 @@ export function createEntityManager(): EntityManager {
495
550
  if (hooks) {
496
551
  const pending = hooks.pendingRemove.get(type);
497
552
  if (pending) pending.push(entityId);
553
+ if (hooks.removeCbs.has(type)) {
554
+ const store = arch.components.get(type);
555
+ if (store) {
556
+ const idx = arch.entityToIndex.get(entityId)!;
557
+ if (!removedData.has(entityId)) removedData.set(entityId, new Map());
558
+ removedData.get(entityId)!.set(type, soaRead(store, idx));
559
+ }
560
+ }
498
561
  }
499
562
 
500
563
  if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(entityId);
@@ -511,9 +574,9 @@ export function createEntityManager(): EntityManager {
511
574
  const newArch = getOrCreateArchetype(newTypes);
512
575
 
513
576
  const idx = arch.entityToIndex.get(entityId)!;
514
- const map: any = {};
577
+ const map = new Map<symbol, ComponentData>();
515
578
  for (const t of newTypes) {
516
- map[t as unknown as string] = readComponentData(arch, t, idx);
579
+ map.set(t, readComponentData(arch, t, idx));
517
580
  }
518
581
 
519
582
  removeFromArchetype(arch, entityId);
@@ -521,28 +584,19 @@ export function createEntityManager(): EntityManager {
521
584
  }
522
585
 
523
586
  function flushDeferred(): void {
524
- // Process deferred ops in order. New ops may be added during flush
525
- // (e.g. hooks triggering more structural changes), so use index loop.
526
- while (deferred.length > 0) {
527
- const ops = deferred.splice(0);
528
- for (let i = 0; i < ops.length; i++) {
529
- const op = ops[i];
530
- switch (op.kind) {
531
- case 'add':
532
- doAddComponent(op.entityId, op.comp, op.data);
533
- break;
534
- case 'remove':
535
- doRemoveComponent(op.entityId, op.comp);
536
- break;
537
- case 'destroy':
538
- doDestroyEntity(op.entityId);
539
- break;
540
- }
587
+ const ops = deferred.splice(0);
588
+ for (const op of ops) {
589
+ switch (op.kind) {
590
+ case 'add': doAddComponent(op.entityId, op.comp, op.data); break;
591
+ case 'remove': doRemoveComponent(op.entityId, op.comp); break;
592
+ case 'destroy': doDestroyEntity(op.entityId); break;
541
593
  }
542
594
  }
543
595
  }
544
596
 
545
597
  return {
598
+ wasmMemory: arena ? arena.memory : null,
599
+
546
600
  createEntity(): EntityId {
547
601
  const id = nextId++;
548
602
  allEntityIds.add(id);
@@ -557,7 +611,7 @@ export function createEntityManager(): EntityManager {
557
611
  doDestroyEntity(id);
558
612
  },
559
613
 
560
- addComponent(entityId: EntityId, comp: ComponentDef, data?: any): void {
614
+ addComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
561
615
  if (iterating > 0) {
562
616
  // In-place overwrite (entity already has component) is safe — no migration
563
617
  const type = toSym(comp);
@@ -585,30 +639,43 @@ export function createEntityManager(): EntityManager {
585
639
  doRemoveComponent(entityId, comp);
586
640
  },
587
641
 
588
- getComponent(entityId: EntityId, comp: ComponentDef): any {
642
+ getComponent(entityId: EntityId, comp: ComponentDef): Record<string, number | string | number[]> | undefined {
589
643
  const type = toSym(comp);
590
644
  const arch = entityArchetype.get(entityId);
591
- if (!arch) return undefined;
592
- const idx = arch.entityToIndex.get(entityId);
593
- if (idx === undefined) return undefined;
594
- return readComponentData(arch, type, idx);
645
+ if (arch) {
646
+ const idx = arch.entityToIndex.get(entityId);
647
+ if (idx !== undefined) return readComponentData(arch, type, idx);
648
+ }
649
+ // Fallback: check recently-removed data (accessible during @OnRemoved hooks)
650
+ const removed = removedData.get(entityId);
651
+ if (removed) return removed.get(type);
652
+ return undefined;
595
653
  },
596
654
 
597
- get(entityId: EntityId, fieldRef: FieldRef): any {
655
+ get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined {
598
656
  const arch = entityArchetype.get(entityId);
599
- if (!arch) return undefined;
600
- const store = arch.components.get(fieldRef._sym);
601
- if (!store) return undefined;
602
- const idx = arch.entityToIndex.get(entityId)!;
603
- const size = store._arraySizes[fieldRef._field] || 0;
604
- if (size > 0) {
605
- const base = idx * size;
606
- return store[fieldRef._field].subarray(base, base + size);
657
+ if (arch) {
658
+ const store = arch.components.get(fieldRef._sym);
659
+ if (store) {
660
+ const idx = arch.entityToIndex.get(entityId)!;
661
+ const size = store._arraySizes[fieldRef._field] || 0;
662
+ if (size > 0) {
663
+ const base = idx * size;
664
+ return (store._fields[fieldRef._field] as Float32Array).subarray(base, base + size) as unknown as number;
665
+ }
666
+ return (store._fields[fieldRef._field] as never[])[idx];
667
+ }
668
+ }
669
+ // Fallback: check recently-removed data (accessible during @OnRemoved hooks)
670
+ const removed = removedData.get(entityId);
671
+ if (removed) {
672
+ const compData = removed.get(fieldRef._sym);
673
+ if (compData) return compData[fieldRef._field] as number | string;
607
674
  }
608
- return store[fieldRef._field][idx];
675
+ return undefined;
609
676
  },
610
677
 
611
- set(entityId: EntityId, fieldRef: FieldRef, value: any): void {
678
+ set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void {
612
679
  const arch = entityArchetype.get(entityId);
613
680
  if (!arch) return;
614
681
  const store = arch.components.get(fieldRef._sym);
@@ -616,9 +683,9 @@ export function createEntityManager(): EntityManager {
616
683
  const idx = arch.entityToIndex.get(entityId)!;
617
684
  const size = store._arraySizes[fieldRef._field] || 0;
618
685
  if (size > 0) {
619
- store[fieldRef._field].set(value, idx * size);
686
+ (store._fields[fieldRef._field] as Float32Array).set(value as ArrayLike<number>, idx * size);
620
687
  } else {
621
- store[fieldRef._field][idx] = value;
688
+ (store._fields[fieldRef._field] as never[])[idx] = value as never;
622
689
  }
623
690
  },
624
691
 
@@ -650,11 +717,11 @@ export function createEntityManager(): EntityManager {
650
717
  allEntityIds.add(id);
651
718
 
652
719
  const types: symbol[] = [];
653
- const map: any = {};
720
+ const map = new Map<symbol, ComponentData>();
654
721
  for (let i = 0; i < args.length; i += 2) {
655
722
  const sym = toSym(args[i] as ComponentDef);
656
723
  types.push(sym);
657
- map[sym as unknown as string] = args[i + 1];
724
+ map.set(sym, args[i + 1] as ComponentData);
658
725
  }
659
726
  const arch = getOrCreateArchetype(types);
660
727
  addToArchetype(arch, id, map);
@@ -694,21 +761,40 @@ export function createEntityManager(): EntityManager {
694
761
  snapshotEntityIds: arch.snapshotEntityIds,
695
762
  snapshotCount: arch.snapshotCount,
696
763
  field(ref: FieldRef) {
697
- const sym = ref._sym || ref;
698
- const store = arch.components.get(sym as symbol);
764
+ const store = arch.components.get(ref._sym);
699
765
  if (!store) return undefined;
700
- return store[ref._field];
766
+ return store._fields[ref._field];
701
767
  },
702
768
  fieldStride(ref: FieldRef) {
703
- const sym = ref._sym || ref;
704
- const store = arch.components.get(sym as symbol);
769
+ const store = arch.components.get(ref._sym);
705
770
  if (!store) return 1;
706
771
  return store._arraySizes[ref._field] || 1;
707
772
  },
773
+ fieldOffset(ref: FieldRef) {
774
+ if (!arena) return -1;
775
+ const store = arch.components.get(ref._sym);
776
+ if (!store) return -1;
777
+ const arr = store._fields[ref._field];
778
+ if (!arr || arr instanceof Array) return -1;
779
+ return (arr as Exclude<SoAArrayValue, unknown[]>).byteOffset;
780
+ },
781
+ fieldAdd(target: FieldRef, source: FieldRef) {
782
+ const tStore = arch.components.get(target._sym);
783
+ const sStore = arch.components.get(source._sym);
784
+ if (!tStore || !sStore) return;
785
+ const dst = tStore._fields[target._field];
786
+ const src = sStore._fields[source._field];
787
+ const stride = tStore._arraySizes[target._field] || 1;
788
+ const n = arch.count * stride;
789
+ if (kernels && dst instanceof Float32Array && src instanceof Float32Array) {
790
+ kernels.add_f32(dst.byteOffset, src.byteOffset, n);
791
+ } else {
792
+ for (let i = 0; i < n; i++) (dst as never[])[i] = ((dst as never[])[i] as number + ((src as never[])[i] as number)) as never;
793
+ }
794
+ },
708
795
  snapshot(ref: FieldRef) {
709
796
  if (!snaps) return undefined;
710
- const sym = ref._sym || ref;
711
- const snap = snaps.get(sym as symbol);
797
+ const snap = snaps.get(ref._sym);
712
798
  if (!snap) return undefined;
713
799
  return snap[ref._field];
714
800
  }
@@ -737,7 +823,7 @@ export function createEntityManager(): EntityManager {
737
823
  arch.snapshotCount = 0;
738
824
  for (const [t, store] of arch.components) {
739
825
  if (store) {
740
- arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
826
+ arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity, arena));
741
827
  }
742
828
  }
743
829
  trackedArchetypes.push(arch);
@@ -765,14 +851,14 @@ export function createEntityManager(): EntityManager {
765
851
  const snap = arch.snapshots!.get(type);
766
852
  if (!snap) continue;
767
853
  for (const field in store._schema) {
768
- const src = store[field];
854
+ const src = store._fields[field];
769
855
  const dst = snap[field];
770
856
  const size = store._arraySizes[field] || 0;
771
857
  const len = size > 0 ? count * size : count;
772
- if (src.set) {
773
- dst.set(src.subarray(0, len));
858
+ if ('set' in src) {
859
+ (dst as Exclude<SoAArrayValue, unknown[]>).set((src as Float32Array).subarray(0, len));
774
860
  } else {
775
- for (let i = 0; i < len; i++) dst[i] = src[i];
861
+ for (let i = 0; i < len; i++) (dst as unknown[])[i] = (src as unknown[])[i];
776
862
  }
777
863
  }
778
864
  }
@@ -855,6 +941,10 @@ export function createEntityManager(): EntityManager {
855
941
  }
856
942
  },
857
943
 
944
+ commitRemovals(): void {
945
+ removedData.clear();
946
+ },
947
+
858
948
  serialize(
859
949
  symbolToName: Map<symbol, string>,
860
950
  stripComponents: ComponentDef[] = [],
@@ -934,11 +1024,11 @@ export function createEntityManager(): EntityManager {
934
1024
 
935
1025
  nextId = data.nextId;
936
1026
 
937
- const entityComponents = new Map<EntityId, any>();
1027
+ const entityComponents = new Map<EntityId, Map<symbol, ComponentData>>();
938
1028
 
939
1029
  for (const id of data.entities) {
940
1030
  allEntityIds.add(id);
941
- entityComponents.set(id, {});
1031
+ entityComponents.set(id, new Map());
942
1032
  }
943
1033
 
944
1034
  for (const [name, store] of Object.entries(data.components)) {
@@ -954,16 +1044,16 @@ export function createEntityManager(): EntityManager {
954
1044
  if (!obj) continue;
955
1045
 
956
1046
  if (customDeserializer) {
957
- obj[sym] = customDeserializer(compData);
1047
+ obj.set(sym, customDeserializer(compData) as ComponentData);
958
1048
  } else {
959
- obj[sym] = compData;
1049
+ obj.set(sym, compData as ComponentData);
960
1050
  }
961
1051
  }
962
1052
  }
963
1053
 
964
- const groupedByKey = new Map<string, { entityId: EntityId; compMap: any }[]>();
1054
+ const groupedByKey = new Map<string, { entityId: EntityId; compMap: Map<symbol, ComponentData> }[]>();
965
1055
  for (const [entityId, compMap] of entityComponents) {
966
- const types = Object.getOwnPropertySymbols(compMap);
1056
+ const types = [...compMap.keys()];
967
1057
  if (types.length === 0) continue;
968
1058
 
969
1059
  const key = maskKey(computeMask(types));
@@ -974,7 +1064,7 @@ export function createEntityManager(): EntityManager {
974
1064
  }
975
1065
 
976
1066
  for (const [, entries] of groupedByKey) {
977
- const types = Object.getOwnPropertySymbols(entries[0].compMap);
1067
+ const types = [...entries[0].compMap.keys()];
978
1068
  const arch = getOrCreateArchetype(types);
979
1069
 
980
1070
  for (const { entityId, compMap } of entries) {