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,4 +1,6 @@
1
1
  import { TYPED, componentSchemas, toSym } from './ComponentRegistry.js';
2
+ import { WasmArena } from './WasmArena.js';
3
+ import { instantiateKernelsSync, isWasmSimdAvailable } from './wasm-kernels.js';
2
4
  const INITIAL_CAPACITY = 64;
3
5
  // ── Array-based bitmask helpers ──────────────────────────
4
6
  function slotsNeeded(bitCount) {
@@ -57,84 +59,101 @@ function unpackSpec(spec) {
57
59
  return spec;
58
60
  return [spec, 0];
59
61
  }
60
- function createSoAStore(schema, capacity) {
61
- const store = { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: {} };
62
+ function createSoAStore(schema, capacity, arena) {
63
+ const fields = {};
64
+ const arraySizes = {};
62
65
  for (const [field, spec] of Object.entries(schema)) {
63
66
  const [Ctor, size] = unpackSpec(spec);
64
- if (size > 0) {
65
- store[field] = new Ctor(capacity * size);
66
- store._arraySizes[field] = size;
67
+ const count = size > 0 ? capacity * size : capacity;
68
+ if (size > 0)
69
+ arraySizes[field] = size;
70
+ if (arena && Ctor !== Array) {
71
+ const NumCtor = Ctor;
72
+ const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
73
+ arena.createView(NumCtor, offset, count, fields, field);
67
74
  }
68
75
  else {
69
- store[field] = new Ctor(capacity);
76
+ fields[field] = new Ctor(count);
70
77
  }
71
78
  }
72
- return store;
79
+ return { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: arraySizes, _fields: fields };
73
80
  }
74
- function growSoAStore(store, newCapacity) {
81
+ function growSoAStore(store, newCapacity, arena) {
75
82
  store._capacity = newCapacity;
76
83
  for (const [field, spec] of Object.entries(store._schema)) {
77
84
  const [Ctor, size] = unpackSpec(spec);
78
- const old = store[field];
85
+ const old = store._fields[field];
79
86
  const allocSize = size > 0 ? newCapacity * size : newCapacity;
80
- store[field] = new Ctor(allocSize);
81
- if (Ctor === Array) {
82
- for (let i = 0; i < old.length; i++)
83
- store[field][i] = old[i];
87
+ if (arena && Ctor !== Array) {
88
+ const NumCtor = Ctor;
89
+ const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
90
+ arena.updateView(store._fields, field, offset, NumCtor, allocSize);
91
+ store._fields[field].set(old);
84
92
  }
85
93
  else {
86
- store[field].set(old);
94
+ const grown = new Ctor(allocSize);
95
+ if (Ctor === Array) {
96
+ for (let i = 0; i < old.length; i++)
97
+ grown[i] = old[i];
98
+ }
99
+ else {
100
+ grown.set(old);
101
+ }
102
+ store._fields[field] = grown;
87
103
  }
88
104
  }
89
105
  }
90
106
  function soaWrite(store, idx, data) {
91
107
  if (!data) {
92
108
  for (const field in store._schema) {
109
+ const arr = store._fields[field];
93
110
  const size = store._arraySizes[field] || 0;
94
111
  if (size > 0) {
95
112
  const base = idx * size;
96
113
  for (let j = 0; j < size; j++)
97
- store[field][base + j] = 0;
114
+ arr[base + j] = 0;
98
115
  }
99
116
  else {
100
- store[field][idx] = 0;
117
+ arr[idx] = 0;
101
118
  }
102
119
  }
103
120
  return;
104
121
  }
105
122
  for (const field in store._schema) {
123
+ const arr = store._fields[field];
106
124
  const size = store._arraySizes[field] || 0;
107
125
  if (size > 0) {
108
126
  const base = idx * size;
109
127
  const src = data[field];
110
128
  if (src) {
111
129
  for (let j = 0; j < size; j++) {
112
- store[field][base + j] = src[j] ?? 0;
130
+ arr[base + j] = (src[j] ?? 0);
113
131
  }
114
132
  }
115
133
  }
116
134
  else {
117
- store[field][idx] = data[field];
135
+ arr[idx] = data[field];
118
136
  }
119
137
  }
120
138
  }
121
139
  function soaRead(store, idx) {
122
140
  const obj = {};
123
141
  for (const field in store._schema) {
142
+ const arr = store._fields[field];
124
143
  const size = store._arraySizes[field] || 0;
125
144
  if (size > 0) {
126
145
  const base = idx * size;
127
- obj[field] = Array.from(store[field].subarray(base, base + size));
146
+ obj[field] = Array.from(arr.subarray(base, base + size));
128
147
  }
129
148
  else {
130
- obj[field] = store[field][idx];
149
+ obj[field] = arr[idx];
131
150
  }
132
151
  }
133
152
  return obj;
134
153
  }
135
154
  function soaSwap(store, idxA, idxB) {
136
155
  for (const field in store._schema) {
137
- const arr = store[field];
156
+ const arr = store._fields[field];
138
157
  const size = store._arraySizes[field] || 0;
139
158
  if (size > 0) {
140
159
  const baseA = idxA * size;
@@ -152,29 +171,51 @@ function soaSwap(store, idxA, idxB) {
152
171
  }
153
172
  }
154
173
  }
155
- function createSnapshotStore(schema, capacity) {
174
+ function createSnapshotStore(schema, capacity, arena) {
156
175
  const snap = {};
157
176
  for (const [field, spec] of Object.entries(schema)) {
158
177
  const [Ctor, size] = unpackSpec(spec);
159
- snap[field] = new Ctor(size > 0 ? capacity * size : capacity);
178
+ const count = size > 0 ? capacity * size : capacity;
179
+ if (arena && Ctor !== Array) {
180
+ const NumCtor = Ctor;
181
+ const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
182
+ arena.createView(NumCtor, offset, count, snap, field);
183
+ }
184
+ else {
185
+ snap[field] = new Ctor(count);
186
+ }
160
187
  }
161
188
  return snap;
162
189
  }
163
- function growSnapshotStore(snap, schema, newCapacity) {
190
+ function growSnapshotStore(snap, schema, newCapacity, arena) {
164
191
  for (const [field, spec] of Object.entries(schema)) {
165
192
  const [Ctor, size] = unpackSpec(spec);
166
193
  const old = snap[field];
167
- snap[field] = new Ctor(size > 0 ? newCapacity * size : newCapacity);
168
- if (Ctor === Array) {
169
- for (let i = 0; i < old.length; i++)
170
- snap[field][i] = old[i];
194
+ const allocSize = size > 0 ? newCapacity * size : newCapacity;
195
+ if (arena && Ctor !== Array) {
196
+ const NumCtor = Ctor;
197
+ const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
198
+ arena.updateView(snap, field, offset, NumCtor, allocSize);
199
+ snap[field].set(old);
171
200
  }
172
201
  else {
173
- snap[field].set(old);
202
+ const grown = new Ctor(allocSize);
203
+ if (Ctor === Array) {
204
+ for (let i = 0; i < old.length; i++)
205
+ grown[i] = old[i];
206
+ }
207
+ else {
208
+ grown.set(old);
209
+ }
210
+ snap[field] = grown;
174
211
  }
175
212
  }
176
213
  }
177
- export function createEntityManager() {
214
+ // ── Entity Manager ───────────────────────────────────────
215
+ export function createEntityManager(options) {
216
+ const useWasm = options?.wasm ?? isWasmSimdAvailable();
217
+ const arena = useWasm ? new WasmArena() : undefined;
218
+ const kernels = arena ? instantiateKernelsSync(arena.memory) : null;
178
219
  let nextId = 1;
179
220
  let nextArchId = 1;
180
221
  const allEntityIds = new Set();
@@ -183,7 +224,7 @@ export function createEntityManager() {
183
224
  let destroyedSet = null;
184
225
  const trackedArchetypes = [];
185
226
  let hooks = null;
186
- // Deferred structural changes during forEach iteration
227
+ const removedData = new Map();
187
228
  let iterating = 0;
188
229
  const deferred = [];
189
230
  const componentBitIndex = new Map();
@@ -230,10 +271,10 @@ export function createEntityManager() {
230
271
  };
231
272
  for (const t of types) {
232
273
  const schema = componentSchemas.get(t);
233
- const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
274
+ const store = schema ? createSoAStore(schema, INITIAL_CAPACITY, arena) : null;
234
275
  arch.components.set(t, store);
235
276
  if (tracked && store) {
236
- arch.snapshots.set(t, createSnapshotStore(schema, INITIAL_CAPACITY));
277
+ arch.snapshots.set(t, createSnapshotStore(schema, INITIAL_CAPACITY, arena));
237
278
  }
238
279
  }
239
280
  archetypes.set(key, arch);
@@ -250,11 +291,11 @@ export function createEntityManager() {
250
291
  arch.capacity = newCap;
251
292
  for (const [type, store] of arch.components) {
252
293
  if (store) {
253
- growSoAStore(store, newCap);
294
+ growSoAStore(store, newCap, arena);
254
295
  if (arch.snapshots) {
255
296
  const snap = arch.snapshots.get(type);
256
297
  if (snap)
257
- growSnapshotStore(snap, store._schema, newCap);
298
+ growSnapshotStore(snap, store._schema, newCap, arena);
258
299
  }
259
300
  }
260
301
  }
@@ -266,7 +307,7 @@ export function createEntityManager() {
266
307
  for (const t of arch.types) {
267
308
  const store = arch.components.get(t);
268
309
  if (store)
269
- soaWrite(store, idx, componentMap[t]);
310
+ soaWrite(store, idx, componentMap.get(t));
270
311
  }
271
312
  arch.entityToIndex.set(entityId, idx);
272
313
  arch.count++;
@@ -313,7 +354,6 @@ export function createEntityManager() {
313
354
  queryCache.set(queryStr, { version: queryCacheVersion, archetypes: matching });
314
355
  return matching;
315
356
  }
316
- // ── Internal structural change implementations ──────────
317
357
  function doDestroyEntity(id) {
318
358
  const arch = entityArchetype.get(id);
319
359
  if (arch) {
@@ -323,6 +363,22 @@ export function createEntityManager() {
323
363
  if (pending)
324
364
  pending.push(id);
325
365
  }
366
+ if (hooks.removeCbs.size > 0) {
367
+ const idx = arch.entityToIndex.get(id);
368
+ let entitySnap;
369
+ for (const type of arch.types) {
370
+ if (hooks.removeCbs.has(type)) {
371
+ const store = arch.components.get(type);
372
+ if (store) {
373
+ if (!entitySnap) {
374
+ entitySnap = new Map();
375
+ removedData.set(id, entitySnap);
376
+ }
377
+ entitySnap.set(type, soaRead(store, idx));
378
+ }
379
+ }
380
+ }
381
+ }
326
382
  }
327
383
  if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter))
328
384
  destroyedSet.add(id);
@@ -335,7 +391,7 @@ export function createEntityManager() {
335
391
  const arch = entityArchetype.get(entityId);
336
392
  if (!arch) {
337
393
  const newArch = getOrCreateArchetype([type]);
338
- addToArchetype(newArch, entityId, { [type]: data });
394
+ addToArchetype(newArch, entityId, new Map([[type, data]]));
339
395
  if (hooks) {
340
396
  const pending = hooks.pendingAdd.get(type);
341
397
  if (pending)
@@ -354,9 +410,9 @@ export function createEntityManager() {
354
410
  const newTypes = [...arch.types, type];
355
411
  const newArch = getOrCreateArchetype(newTypes);
356
412
  const idx = arch.entityToIndex.get(entityId);
357
- const map = { [type]: data };
413
+ const map = new Map([[type, data]]);
358
414
  for (const t of arch.types) {
359
- map[t] = readComponentData(arch, t, idx);
415
+ map.set(t, readComponentData(arch, t, idx));
360
416
  }
361
417
  removeFromArchetype(arch, entityId);
362
418
  addToArchetype(newArch, entityId, map);
@@ -375,6 +431,15 @@ export function createEntityManager() {
375
431
  const pending = hooks.pendingRemove.get(type);
376
432
  if (pending)
377
433
  pending.push(entityId);
434
+ if (hooks.removeCbs.has(type)) {
435
+ const store = arch.components.get(type);
436
+ if (store) {
437
+ const idx = arch.entityToIndex.get(entityId);
438
+ if (!removedData.has(entityId))
439
+ removedData.set(entityId, new Map());
440
+ removedData.get(entityId).set(type, soaRead(store, idx));
441
+ }
442
+ }
378
443
  }
379
444
  if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter))
380
445
  destroyedSet.add(entityId);
@@ -389,35 +454,31 @@ export function createEntityManager() {
389
454
  }
390
455
  const newArch = getOrCreateArchetype(newTypes);
391
456
  const idx = arch.entityToIndex.get(entityId);
392
- const map = {};
457
+ const map = new Map();
393
458
  for (const t of newTypes) {
394
- map[t] = readComponentData(arch, t, idx);
459
+ map.set(t, readComponentData(arch, t, idx));
395
460
  }
396
461
  removeFromArchetype(arch, entityId);
397
462
  addToArchetype(newArch, entityId, map);
398
463
  }
399
464
  function flushDeferred() {
400
- // Process deferred ops in order. New ops may be added during flush
401
- // (e.g. hooks triggering more structural changes), so use index loop.
402
- while (deferred.length > 0) {
403
- const ops = deferred.splice(0);
404
- for (let i = 0; i < ops.length; i++) {
405
- const op = ops[i];
406
- switch (op.kind) {
407
- case 'add':
408
- doAddComponent(op.entityId, op.comp, op.data);
409
- break;
410
- case 'remove':
411
- doRemoveComponent(op.entityId, op.comp);
412
- break;
413
- case 'destroy':
414
- doDestroyEntity(op.entityId);
415
- break;
416
- }
465
+ const ops = deferred.splice(0);
466
+ for (const op of ops) {
467
+ switch (op.kind) {
468
+ case 'add':
469
+ doAddComponent(op.entityId, op.comp, op.data);
470
+ break;
471
+ case 'remove':
472
+ doRemoveComponent(op.entityId, op.comp);
473
+ break;
474
+ case 'destroy':
475
+ doDestroyEntity(op.entityId);
476
+ break;
417
477
  }
418
478
  }
419
479
  }
420
480
  return {
481
+ wasmMemory: arena ? arena.memory : null,
421
482
  createEntity() {
422
483
  const id = nextId++;
423
484
  allEntityIds.add(id);
@@ -459,27 +520,39 @@ export function createEntityManager() {
459
520
  getComponent(entityId, comp) {
460
521
  const type = toSym(comp);
461
522
  const arch = entityArchetype.get(entityId);
462
- if (!arch)
463
- return undefined;
464
- const idx = arch.entityToIndex.get(entityId);
465
- if (idx === undefined)
466
- return undefined;
467
- return readComponentData(arch, type, idx);
523
+ if (arch) {
524
+ const idx = arch.entityToIndex.get(entityId);
525
+ if (idx !== undefined)
526
+ return readComponentData(arch, type, idx);
527
+ }
528
+ // Fallback: check recently-removed data (accessible during @OnRemoved hooks)
529
+ const removed = removedData.get(entityId);
530
+ if (removed)
531
+ return removed.get(type);
532
+ return undefined;
468
533
  },
469
534
  get(entityId, fieldRef) {
470
535
  const arch = entityArchetype.get(entityId);
471
- if (!arch)
472
- return undefined;
473
- const store = arch.components.get(fieldRef._sym);
474
- if (!store)
475
- return undefined;
476
- const idx = arch.entityToIndex.get(entityId);
477
- const size = store._arraySizes[fieldRef._field] || 0;
478
- if (size > 0) {
479
- const base = idx * size;
480
- return store[fieldRef._field].subarray(base, base + size);
536
+ if (arch) {
537
+ const store = arch.components.get(fieldRef._sym);
538
+ if (store) {
539
+ const idx = arch.entityToIndex.get(entityId);
540
+ const size = store._arraySizes[fieldRef._field] || 0;
541
+ if (size > 0) {
542
+ const base = idx * size;
543
+ return store._fields[fieldRef._field].subarray(base, base + size);
544
+ }
545
+ return store._fields[fieldRef._field][idx];
546
+ }
481
547
  }
482
- return store[fieldRef._field][idx];
548
+ // Fallback: check recently-removed data (accessible during @OnRemoved hooks)
549
+ const removed = removedData.get(entityId);
550
+ if (removed) {
551
+ const compData = removed.get(fieldRef._sym);
552
+ if (compData)
553
+ return compData[fieldRef._field];
554
+ }
555
+ return undefined;
483
556
  },
484
557
  set(entityId, fieldRef, value) {
485
558
  const arch = entityArchetype.get(entityId);
@@ -491,10 +564,10 @@ export function createEntityManager() {
491
564
  const idx = arch.entityToIndex.get(entityId);
492
565
  const size = store._arraySizes[fieldRef._field] || 0;
493
566
  if (size > 0) {
494
- store[fieldRef._field].set(value, idx * size);
567
+ store._fields[fieldRef._field].set(value, idx * size);
495
568
  }
496
569
  else {
497
- store[fieldRef._field][idx] = value;
570
+ store._fields[fieldRef._field][idx] = value;
498
571
  }
499
572
  },
500
573
  hasComponent(entityId, comp) {
@@ -521,11 +594,11 @@ export function createEntityManager() {
521
594
  const id = nextId++;
522
595
  allEntityIds.add(id);
523
596
  const types = [];
524
- const map = {};
597
+ const map = new Map();
525
598
  for (let i = 0; i < args.length; i += 2) {
526
599
  const sym = toSym(args[i]);
527
600
  types.push(sym);
528
- map[sym] = args[i + 1];
601
+ map.set(sym, args[i + 1]);
529
602
  }
530
603
  const arch = getOrCreateArchetype(types);
531
604
  addToArchetype(arch, id, map);
@@ -564,24 +637,49 @@ export function createEntityManager() {
564
637
  snapshotEntityIds: arch.snapshotEntityIds,
565
638
  snapshotCount: arch.snapshotCount,
566
639
  field(ref) {
567
- const sym = ref._sym || ref;
568
- const store = arch.components.get(sym);
640
+ const store = arch.components.get(ref._sym);
569
641
  if (!store)
570
642
  return undefined;
571
- return store[ref._field];
643
+ return store._fields[ref._field];
572
644
  },
573
645
  fieldStride(ref) {
574
- const sym = ref._sym || ref;
575
- const store = arch.components.get(sym);
646
+ const store = arch.components.get(ref._sym);
576
647
  if (!store)
577
648
  return 1;
578
649
  return store._arraySizes[ref._field] || 1;
579
650
  },
651
+ fieldOffset(ref) {
652
+ if (!arena)
653
+ return -1;
654
+ const store = arch.components.get(ref._sym);
655
+ if (!store)
656
+ return -1;
657
+ const arr = store._fields[ref._field];
658
+ if (!arr || arr instanceof Array)
659
+ return -1;
660
+ return arr.byteOffset;
661
+ },
662
+ fieldAdd(target, source) {
663
+ const tStore = arch.components.get(target._sym);
664
+ const sStore = arch.components.get(source._sym);
665
+ if (!tStore || !sStore)
666
+ return;
667
+ const dst = tStore._fields[target._field];
668
+ const src = sStore._fields[source._field];
669
+ const stride = tStore._arraySizes[target._field] || 1;
670
+ const n = arch.count * stride;
671
+ if (kernels && dst instanceof Float32Array && src instanceof Float32Array) {
672
+ kernels.add_f32(dst.byteOffset, src.byteOffset, n);
673
+ }
674
+ else {
675
+ for (let i = 0; i < n; i++)
676
+ dst[i] = (dst[i] + src[i]);
677
+ }
678
+ },
580
679
  snapshot(ref) {
581
680
  if (!snaps)
582
681
  return undefined;
583
- const sym = ref._sym || ref;
584
- const snap = snaps.get(sym);
682
+ const snap = snaps.get(ref._sym);
585
683
  if (!snap)
586
684
  return undefined;
587
685
  return snap[ref._field];
@@ -611,7 +709,7 @@ export function createEntityManager() {
611
709
  arch.snapshotCount = 0;
612
710
  for (const [t, store] of arch.components) {
613
711
  if (store) {
614
- arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
712
+ arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity, arena));
615
713
  }
616
714
  }
617
715
  trackedArchetypes.push(arch);
@@ -640,11 +738,11 @@ export function createEntityManager() {
640
738
  if (!snap)
641
739
  continue;
642
740
  for (const field in store._schema) {
643
- const src = store[field];
741
+ const src = store._fields[field];
644
742
  const dst = snap[field];
645
743
  const size = store._arraySizes[field] || 0;
646
744
  const len = size > 0 ? count * size : count;
647
- if (src.set) {
745
+ if ('set' in src) {
648
746
  dst.set(src.subarray(0, len));
649
747
  }
650
748
  else {
@@ -739,6 +837,9 @@ export function createEntityManager() {
739
837
  pending.length = 0;
740
838
  }
741
839
  },
840
+ commitRemovals() {
841
+ removedData.clear();
842
+ },
742
843
  serialize(symbolToName, stripComponents = [], skipEntitiesWith = [], { serializers } = {}) {
743
844
  const stripSymbols = new Set(stripComponents.map(toSym));
744
845
  const skipSymbols = new Set(skipEntitiesWith.map(toSym));
@@ -809,7 +910,7 @@ export function createEntityManager() {
809
910
  const entityComponents = new Map();
810
911
  for (const id of data.entities) {
811
912
  allEntityIds.add(id);
812
- entityComponents.set(id, {});
913
+ entityComponents.set(id, new Map());
813
914
  }
814
915
  for (const [name, store] of Object.entries(data.components)) {
815
916
  const entry = nameToSymbol[name];
@@ -823,16 +924,16 @@ export function createEntityManager() {
823
924
  if (!obj)
824
925
  continue;
825
926
  if (customDeserializer) {
826
- obj[sym] = customDeserializer(compData);
927
+ obj.set(sym, customDeserializer(compData));
827
928
  }
828
929
  else {
829
- obj[sym] = compData;
930
+ obj.set(sym, compData);
830
931
  }
831
932
  }
832
933
  }
833
934
  const groupedByKey = new Map();
834
935
  for (const [entityId, compMap] of entityComponents) {
835
- const types = Object.getOwnPropertySymbols(compMap);
936
+ const types = [...compMap.keys()];
836
937
  if (types.length === 0)
837
938
  continue;
838
939
  const key = maskKey(computeMask(types));
@@ -842,7 +943,7 @@ export function createEntityManager() {
842
943
  groupedByKey.get(key).push({ entityId, compMap });
843
944
  }
844
945
  for (const [, entries] of groupedByKey) {
845
- const types = Object.getOwnPropertySymbols(entries[0].compMap);
946
+ const types = [...entries[0].compMap.keys()];
846
947
  const arch = getOrCreateArchetype(types);
847
948
  for (const { entityId, compMap } of entries) {
848
949
  addToArchetype(arch, entityId, compMap);
@@ -14,6 +14,8 @@ export declare class System {
14
14
  _registerHook(kind: 'add' | 'remove', types: ComponentDef[], handler: (id: EntityId) => void): void;
15
15
  forEach(types: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
16
16
  tick?(): void;
17
+ /** Process hooks and tick without clearing removed-data snapshots. */
18
+ _runCore(): void;
17
19
  run(): void;
18
20
  dispose(): void;
19
21
  }
@@ -26,6 +28,8 @@ export interface SystemContext {
26
28
  export type FunctionalSystemConstructor = (sys: SystemContext) => (() => void) | void;
27
29
  export interface FunctionalSystem {
28
30
  (): void;
31
+ /** @internal Process hooks and tick without clearing removed-data snapshots. */
32
+ _runCore(): void;
29
33
  dispose(): void;
30
34
  }
31
35
  export declare function createSystem(em: EntityManager, constructor: FunctionalSystemConstructor): FunctionalSystem;
@@ -47,7 +47,8 @@ export class System {
47
47
  forEach(types, callback, exclude) {
48
48
  this.em.forEach(types, callback, exclude);
49
49
  }
50
- run() {
50
+ /** Process hooks and tick without clearing removed-data snapshots. */
51
+ _runCore() {
51
52
  for (const hook of this._hooks) {
52
53
  for (const id of hook.buffer)
53
54
  hook.handler(id);
@@ -56,6 +57,10 @@ export class System {
56
57
  if (this.tick)
57
58
  this.tick();
58
59
  }
60
+ run() {
61
+ this._runCore();
62
+ this.em.commitRemovals();
63
+ }
59
64
  dispose() {
60
65
  for (const unsub of this._unsubs)
61
66
  unsub();
@@ -101,7 +106,7 @@ export function createSystem(em, constructor) {
101
106
  },
102
107
  };
103
108
  const tick = constructor(sys);
104
- function system() {
109
+ function runCore() {
105
110
  for (const hook of hooks) {
106
111
  for (const id of hook.buffer)
107
112
  hook.callback(id);
@@ -110,6 +115,11 @@ export function createSystem(em, constructor) {
110
115
  if (tick)
111
116
  tick();
112
117
  }
118
+ function system() {
119
+ runCore();
120
+ em.commitRemovals();
121
+ }
122
+ system._runCore = runCore;
113
123
  system.dispose = function () {
114
124
  for (const hook of hooks) {
115
125
  for (const unsub of hook.unsubs)
@@ -119,17 +129,27 @@ export function createSystem(em, constructor) {
119
129
  };
120
130
  return system;
121
131
  }
132
+ function isSystemClass(entry) {
133
+ let proto = entry.prototype;
134
+ while (proto) {
135
+ proto = Object.getPrototypeOf(proto);
136
+ if (proto === System.prototype)
137
+ return true;
138
+ }
139
+ return false;
140
+ }
122
141
  export function createSystems(em, entries) {
123
142
  const systems = entries.map(Entry => {
124
- if ('prototype' in Entry && Entry.prototype instanceof System) {
143
+ if (isSystemClass(Entry)) {
125
144
  return new Entry(em);
126
145
  }
127
146
  const sys = createSystem(em, Entry);
128
- return { run: sys, dispose: sys.dispose };
147
+ return { _runCore: sys._runCore, dispose: sys.dispose };
129
148
  });
130
149
  function pipeline() {
131
150
  for (let i = 0; i < systems.length; i++)
132
- systems[i].run();
151
+ systems[i]._runCore();
152
+ em.commitRemovals();
133
153
  }
134
154
  pipeline.dispose = function () {
135
155
  for (let i = 0; i < systems.length; i++)
@@ -0,0 +1,13 @@
1
+ import type { SoAArrayValue } from './EntityManager.js';
2
+ export type NumericTypedArrayConstructor = typeof Float32Array | typeof Float64Array | typeof Int8Array | typeof Int16Array | typeof Int32Array | typeof Uint8Array | typeof Uint16Array | typeof Uint32Array;
3
+ export declare class WasmArena {
4
+ memory: WebAssembly.Memory;
5
+ private nextOffset;
6
+ private views;
7
+ constructor(initialPages?: number, maxPages?: number);
8
+ alloc(byteLength: number): number;
9
+ createView<T extends SoAArrayValue>(Ctor: NumericTypedArrayConstructor, offset: number, count: number, fields: Record<string, SoAArrayValue>, field: string): T;
10
+ private handleGrow;
11
+ /** Update a registered view to point to a new (larger) allocation. */
12
+ updateView(fields: Record<string, SoAArrayValue>, field: string, offset: number, Ctor: NumericTypedArrayConstructor, count: number): void;
13
+ }