@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
package/dist/index.mjs CHANGED
@@ -1591,240 +1591,6 @@ function createWorldClock(policy) {
1591
1591
  writer: { time, fixed }
1592
1592
  };
1593
1593
  }
1594
- var SIZE_CLASSES = Object.freeze([
1595
- 16,
1596
- 64,
1597
- 256,
1598
- 1024,
1599
- 4096,
1600
- 16384,
1601
- 65536,
1602
- 262144
1603
- ]);
1604
- var HAS_TRANSFER = typeof ArrayBuffer.prototype.transfer === "function";
1605
- function bucketIndex(byteLength) {
1606
- if (byteLength === 0) return -1;
1607
- for (let i = 0; i < SIZE_CLASSES.length; i++) {
1608
- const b = SIZE_CLASSES[i];
1609
- if (b !== void 0 && byteLength <= b) return i;
1610
- }
1611
- return SIZE_CLASSES.length;
1612
- }
1613
- var BufferPool = class {
1614
- slots = /* @__PURE__ */ new Map();
1615
- /**
1616
- * Per-bucket free-lists: `freeBuckets[i]` is a LIFO stack of slot ids
1617
- * whose backing buffer is parked on bucket `i`. Released slots stay on
1618
- * their original bucket's free-list - v1 never moves a slot between
1619
- * buckets on release (D-7 no trim).
1620
- */
1621
- freeBuckets = SIZE_CLASSES.map(() => []);
1622
- nextId = 1;
1623
- /**
1624
- * Allocate a managed buffer slot of at least `byteLength` bytes.
1625
- *
1626
- * Routes:
1627
- * - invalid byteLength -> structured out-of-bounds error.
1628
- * - byteLength == 0 -> ok({ id, view: zero-length Uint8Array }) (no bucket).
1629
- * - byteLength <= 262144 -> ok({ id, view }), bucket = smallest >= byteLength.
1630
- * - byteLength > 262144 -> a dedicated allocation; allocation failure is structured.
1631
- *
1632
- * D-5: size classes are radix-4 (16 / 64 / 256 / 1K / 4K / 16K / 64K / 256K).
1633
- * Free-list pop reuses the most recently released slot id at the same bucket;
1634
- * miss falls through to a fresh allocation.
1635
- */
1636
- alloc(byteLength) {
1637
- if (byteLength === 0) {
1638
- const id2 = this.nextId++;
1639
- const buffer2 = new ArrayBuffer(0);
1640
- const view2 = new Uint8Array(buffer2);
1641
- this.slots.set(id2, { sizeClassIdx: -1, buffer: buffer2, view: view2, byteLength: 0, live: true });
1642
- return ok({ id: id2, view: view2 });
1643
- }
1644
- if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
1645
- return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
1646
- }
1647
- const idx = bucketIndex(byteLength);
1648
- if (idx === SIZE_CLASSES.length) {
1649
- try {
1650
- const buffer2 = new ArrayBuffer(byteLength);
1651
- const view2 = new Uint8Array(buffer2);
1652
- const id2 = this.nextId++;
1653
- this.slots.set(id2, { sizeClassIdx: idx, buffer: buffer2, view: view2, byteLength, live: true });
1654
- return ok({ id: id2, view: view2 });
1655
- } catch {
1656
- return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
1657
- }
1658
- }
1659
- const bucketBytes = SIZE_CLASSES[idx];
1660
- if (bucketBytes === void 0) {
1661
- return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
1662
- }
1663
- const free = this.freeBuckets[idx];
1664
- if (free !== void 0 && free.length > 0) {
1665
- const id2 = free.pop();
1666
- const slot = this.slots.get(id2);
1667
- if (slot === void 0) {
1668
- return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
1669
- }
1670
- slot.byteLength = byteLength;
1671
- slot.view = new Uint8Array(slot.buffer, 0, byteLength);
1672
- slot.live = true;
1673
- new Uint8Array(slot.buffer).fill(0);
1674
- return ok({ id: id2, view: slot.view });
1675
- }
1676
- const id = this.nextId++;
1677
- const buffer = new ArrayBuffer(bucketBytes);
1678
- const view = new Uint8Array(buffer, 0, byteLength);
1679
- this.slots.set(id, {
1680
- sizeClassIdx: idx,
1681
- buffer,
1682
- view,
1683
- byteLength,
1684
- live: true
1685
- });
1686
- return ok({ id, view });
1687
- }
1688
- /**
1689
- * Grow slot `id` to `newBytes`. Returns the post-grow Uint8Array view.
1690
- *
1691
- * Routes (D-6 / D-7):
1692
- * - newBytes < current -> err(managed-buffer-shrink-not-supported).
1693
- * - newBytes == current -> ok(current view) (no-op, identity preserved).
1694
- * - newBytes > current && same bucket -> ok(re-sliced view) (no transfer).
1695
- * - newBytes > current && cross bucket -> ok(new view) backed by a fresh
1696
- * bucket buffer; the prior ArrayBuffer is detached via transfer (ES2024)
1697
- * or replaced via allocate-and-copy fallback. Old `view`s captured by
1698
- * the caller become detached / orphaned - callers must use `pool.view(id)`
1699
- * after grow to read the refreshed view (the `release` loop refreshes
1700
- * automatically).
1701
- * - newBytes beyond the last pooled class -> dedicated allocation.
1702
- */
1703
- grow(id, newBytes) {
1704
- const slot = this.slots.get(id);
1705
- if (slot === void 0) {
1706
- return err(new ManagedBufferOutOfBoundsError(newBytes, 0));
1707
- }
1708
- if (newBytes < slot.byteLength) {
1709
- return err(new ManagedBufferShrinkNotSupportedError(newBytes, slot.byteLength));
1710
- }
1711
- if (newBytes === slot.byteLength) {
1712
- return ok(slot.view);
1713
- }
1714
- if (!Number.isSafeInteger(newBytes) || newBytes < 0) {
1715
- return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
1716
- }
1717
- const newIdx = bucketIndex(newBytes);
1718
- if (newBytes <= slot.buffer.byteLength) {
1719
- slot.byteLength = newBytes;
1720
- slot.view = new Uint8Array(slot.buffer, 0, newBytes);
1721
- return ok(slot.view);
1722
- }
1723
- const newBucketBytes = SIZE_CLASSES[newIdx] ?? Math.max(newBytes, slot.buffer.byteLength * 2);
1724
- const oldByteLength = slot.byteLength;
1725
- let nextBuffer;
1726
- try {
1727
- if (HAS_TRANSFER) {
1728
- nextBuffer = slot.buffer.transfer(newBucketBytes);
1729
- } else {
1730
- nextBuffer = new ArrayBuffer(newBucketBytes);
1731
- new Uint8Array(nextBuffer).set(new Uint8Array(slot.buffer, 0, oldByteLength));
1732
- }
1733
- } catch {
1734
- return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
1735
- }
1736
- slot.sizeClassIdx = newIdx;
1737
- slot.buffer = nextBuffer;
1738
- slot.byteLength = newBytes;
1739
- slot.view = new Uint8Array(nextBuffer, 0, newBytes);
1740
- return ok(slot.view);
1741
- }
1742
- /**
1743
- * Release slot `id` back to its bucket's free-list. Releasing an unknown id
1744
- * is a no-op - World's release loop drives this and idempotency keeps the
1745
- * despawn chain free of bookkeeping noise. Bucket free-lists are NEVER
1746
- * trimmed in v1 (D-7 no trim).
1747
- */
1748
- release(id) {
1749
- const slot = this.slots.get(id);
1750
- if (slot === void 0) return ok(void 0);
1751
- if (!slot.live) return ok(void 0);
1752
- slot.live = false;
1753
- if (slot.sizeClassIdx >= 0 && slot.sizeClassIdx < SIZE_CLASSES.length) {
1754
- const bucket = this.freeBuckets[slot.sizeClassIdx];
1755
- if (bucket !== void 0) bucket.push(id);
1756
- } else {
1757
- this.slots.delete(id);
1758
- }
1759
- return ok(void 0);
1760
- }
1761
- /**
1762
- * Return the live view for slot `id`. Used by World after `grow` to
1763
- * refresh the column's stored view reference. Returns a zero-length view
1764
- * for unknown / released ids so callers never crash on use-after-release.
1765
- */
1766
- view(id) {
1767
- const slot = this.slots.get(id);
1768
- if (slot === void 0 || !slot.live) return new Uint8Array(0);
1769
- return slot.view;
1770
- }
1771
- /**
1772
- * Return the bucket-rounded byte capacity for slot `id` --- i.e.
1773
- * `SIZE_CLASSES[slot.sizeClassIdx]`. Used by managed-buffer view callers
1774
- * (D-4 no-cache: re-queried per accessor). Returns `0` for the zero-length
1775
- * (`alloc(0)`) slot; returns `0` for unknown / released ids (mirrors
1776
- * `view(id)` use-after-release semantics).
1777
- */
1778
- byteCapacity(id) {
1779
- const slot = this.slots.get(id);
1780
- if (slot === void 0 || !slot.live) return 0;
1781
- if (slot.sizeClassIdx < 0) return 0;
1782
- return slot.buffer.byteLength;
1783
- }
1784
- /**
1785
- * Reset the logical byteLength of slot `id` to `newByteLength` while
1786
- * keeping the same bucket allocation (no transfer, no release). Only
1787
- * legal when `newByteLength <= bucketBytes`; the slot's bucket index
1788
- * does not move (D-7 v1 forbids cross-bucket shrink). Used by managed-
1789
- * buffer clear paths so the slot retains its `byteCapacity` while the
1790
- * live view becomes zero-length. Bytes past the new logical length are
1791
- * zero-filled defensively.
1792
- *
1793
- * Returns `err(ManagedBufferShrinkNotSupportedError)` when called on a
1794
- * `sizeClassIdx === -1` slot (alloc(0)) with `newByteLength > 0`, or
1795
- * when `newByteLength` exceeds the bucket capacity.
1796
- */
1797
- setLogicalLength(id, newByteLength) {
1798
- const slot = this.slots.get(id);
1799
- if (slot === void 0) {
1800
- return err(new ManagedBufferOutOfBoundsError(newByteLength, 0));
1801
- }
1802
- if (slot.sizeClassIdx < 0) {
1803
- if (newByteLength === 0) {
1804
- slot.byteLength = 0;
1805
- slot.view = new Uint8Array(slot.buffer);
1806
- return ok(slot.view);
1807
- }
1808
- return err(new ManagedBufferOutOfBoundsError(newByteLength, 0));
1809
- }
1810
- const bucketBytes = slot.buffer.byteLength;
1811
- if (newByteLength > bucketBytes) {
1812
- return err(new ManagedBufferOutOfBoundsError(newByteLength, bucketBytes));
1813
- }
1814
- if (newByteLength < slot.byteLength) {
1815
- new Uint8Array(slot.buffer, newByteLength, slot.byteLength - newByteLength).fill(0);
1816
- }
1817
- slot.byteLength = newByteLength;
1818
- slot.view = new Uint8Array(slot.buffer, 0, newByteLength);
1819
- return ok(slot.view);
1820
- }
1821
- /** @internal Diagnostic count of live slots. Exposed for tests + inspector. */
1822
- _liveCount() {
1823
- let n = 0;
1824
- for (const s of this.slots.values()) if (s.live) n += 1;
1825
- return n;
1826
- }
1827
- };
1828
1594
  var ENTITY_MAX_INDEX = MAX_SLOT;
1829
1595
  var ENTITY_MAX_GENERATION = MAX_GEN;
1830
1596
  var ENTITY_NULL_RAW = 4294967295;
@@ -1841,112 +1607,10 @@ function entityGeneration(entity) {
1841
1607
  return unpackGen(entity);
1842
1608
  }
1843
1609
 
1844
- // src/component-default-fallback.ts
1845
- function typeDefault(fieldType) {
1846
- if (fieldType === "bool") return false;
1847
- if (fieldType === "entity") return ENTITY_NULL_RAW;
1848
- if (fieldType === "array<entity>") return [];
1849
- return 0;
1850
- }
1851
- function fillComponentDefaults(token, raw) {
1852
- const schema = componentSchema(token);
1853
- const layer2 = componentDefinition(token).defaults;
1854
- const out = /* @__PURE__ */ Object.create(null);
1855
- const rawObj = raw ?? void 0;
1856
- for (const fieldName of Object.keys(schema)) {
1857
- const fieldType = schema[fieldName];
1858
- if (fieldType === void 0) continue;
1859
- if (rawObj !== void 0 && fieldName in rawObj) {
1860
- out[fieldName] = rawObj[fieldName];
1861
- continue;
1862
- }
1863
- if (layer2 !== void 0 && fieldName in layer2) {
1864
- out[fieldName] = layer2[fieldName];
1865
- continue;
1866
- }
1867
- out[fieldName] = typeDefault(fieldType);
1868
- }
1869
- return out;
1870
- }
1871
- function validateComponentDataKeys(token, raw) {
1872
- if (raw === void 0) return null;
1873
- const schema = componentSchema(token);
1874
- const rawObj = raw;
1875
- for (const fieldName of Object.keys(rawObj)) {
1876
- if (!(fieldName in schema)) {
1877
- return new SpawnDataUnknownFieldError(token.name, fieldName, Object.keys(schema));
1878
- }
1879
- }
1880
- return null;
1881
- }
1882
-
1883
- // src/component-value-validate.ts
1884
- function isNumericHandle(v) {
1885
- return typeof v === "number";
1886
- }
1887
- function isSharedScalarType(fieldType) {
1888
- return fieldType.startsWith("shared<") && fieldType.endsWith(">");
1889
- }
1890
- function isSharedArrayType(fieldType) {
1891
- if (!fieldType.startsWith("array<") || !fieldType.endsWith(">")) return false;
1892
- const inner = fieldType.slice(6, -1);
1893
- const head = inner.indexOf(",") === -1 ? inner : inner.slice(0, inner.indexOf(",")).trim();
1894
- return head.startsWith("shared<") && head.endsWith(">");
1895
- }
1896
- function isArrayPayload(value) {
1897
- if (Array.isArray(value)) return true;
1898
- return ArrayBuffer.isView(value) && typeof value.length === "number";
1899
- }
1900
- function validateManagedArrayValues(token, raw) {
1901
- if (raw === void 0) return null;
1902
- const fields = componentDefinition(token).fields;
1903
- const rawObj = raw;
1904
- for (const fieldName of Object.keys(rawObj)) {
1905
- const reflection = fields[fieldName];
1906
- if (reflection?.arrayMeta === void 0) continue;
1907
- const value = rawObj[fieldName];
1908
- if (value === 0) continue;
1909
- if (value === void 0 || value === null || isArrayPayload(value)) continue;
1910
- return new ManagedArrayInvalidValueError(token.name, fieldName, reflection.type, value);
1911
- }
1912
- return null;
1913
- }
1914
- function validateSharedFieldValues(token, raw) {
1915
- if (raw === void 0) return null;
1916
- const schema = componentSchema(token);
1917
- const rawObj = raw;
1918
- for (const fieldName of Object.keys(rawObj)) {
1919
- const fieldType = schema[fieldName];
1920
- if (fieldType === void 0) continue;
1921
- const value = rawObj[fieldName];
1922
- if (value === void 0 || value === null) continue;
1923
- if (isSharedScalarType(fieldType)) {
1924
- if (!isNumericHandle(value)) {
1925
- return new SharedFieldInvalidValueError(token.name, fieldName, fieldType, value);
1926
- }
1927
- } else if (isSharedArrayType(fieldType)) {
1928
- if (!Array.isArray(value)) continue;
1929
- for (let i = 0; i < value.length; i++) {
1930
- const el = value[i];
1931
- if (el === void 0 || el === null) continue;
1932
- if (!isNumericHandle(el)) {
1933
- return new SharedFieldInvalidValueError(token.name, fieldName, fieldType, el, i);
1934
- }
1935
- }
1936
- }
1937
- }
1938
- return null;
1939
- }
1940
-
1941
- // src/internal.ts
1942
- var DERIVED_WRITER = /* @__PURE__ */ Symbol.for(
1943
- "forgeax.ecs.query.derivedWriter"
1944
- );
1945
-
1946
- // src/storage/archetype.ts
1947
- var INITIAL_CAPACITY = 64;
1948
- function archetypeKey(componentIds) {
1949
- return [...foldEssentials(componentIds)].sort((a, b) => a - b).join("+");
1610
+ // src/storage/archetype.ts
1611
+ var INITIAL_CAPACITY = 64;
1612
+ function archetypeKey(componentIds) {
1613
+ return [...foldEssentials(componentIds)].sort((a, b) => a - b).join("+");
1950
1614
  }
1951
1615
  function createArchetype(components, id, tableId) {
1952
1616
  const byId = /* @__PURE__ */ new Map();
@@ -2014,7 +1678,7 @@ function createColumn(fieldType, capacity, arity = 1, shared = false) {
2014
1678
  const view = createFieldView(Ctor, buffer);
2015
1679
  return { buffer, view, capacity, fieldType, arity };
2016
1680
  }
2017
- var HAS_TRANSFER2 = typeof ArrayBuffer.prototype.transfer === "function";
1681
+ var HAS_TRANSFER = typeof ArrayBuffer.prototype.transfer === "function";
2018
1682
  function growColumn(col, newCapacity) {
2019
1683
  const meta = TYPE_METADATA[col.fieldType];
2020
1684
  if (!meta) throw new Error(`Missing TYPE_METADATA entry for scalar field type ${col.fieldType}`);
@@ -2028,7 +1692,7 @@ function growColumn(col, newCapacity) {
2028
1692
  if (isSharedBuffer(col.buffer)) {
2029
1693
  buffer = new SharedArrayBuffer(newByteLength);
2030
1694
  new Uint8Array(buffer).set(new Uint8Array(col.buffer));
2031
- } else if (HAS_TRANSFER2) {
1695
+ } else if (HAS_TRANSFER) {
2032
1696
  buffer = col.buffer.transfer(
2033
1697
  newByteLength
2034
1698
  );
@@ -2066,7 +1730,7 @@ function canonicalComponents(components) {
2066
1730
  for (const component of components) byId.set(componentId(component), component);
2067
1731
  return [...byId.values()].sort((a, b) => componentId(a) - componentId(b));
2068
1732
  }
2069
- function createTable(components, id, shared = false) {
1733
+ function createTable(components, id, shared = false, activeDirectories = [/* @__PURE__ */ new Set()]) {
2070
1734
  const sortedComponents = canonicalComponents(components);
2071
1735
  const capacity = INITIAL_CAPACITY2;
2072
1736
  const storage = /* @__PURE__ */ new Map();
@@ -2110,19 +1774,26 @@ function createTable(components, id, shared = false) {
2110
1774
  storage,
2111
1775
  size: 0,
2112
1776
  capacity,
2113
- version: 0
1777
+ version: 0,
1778
+ membership: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE)),
1779
+ activeDirectories
2114
1780
  };
2115
1781
  }
2116
- function appendTableRow(table, entity) {
1782
+ function appendTableRow(table, entity, epoch = 0) {
2117
1783
  if (table.size === table.capacity) growTable(table, table.capacity * 2);
2118
1784
  const row = table.size;
2119
1785
  const self = table.storage.get(componentId(Entity))?.fields.get("self");
2120
1786
  if (self !== void 0) self.view[row] = entity;
2121
1787
  table.size = row + 1;
1788
+ if (row === 0) for (const directory of table.activeDirectories) directory.add(table);
1789
+ markTableMembership(table, row, epoch);
2122
1790
  return row;
2123
1791
  }
2124
- function removeTableRow(table, row) {
1792
+ function removeTableRow(table, row, epoch = 0) {
2125
1793
  const lastRow = table.size - 1;
1794
+ markTableMembership(table, row, epoch);
1795
+ markTableMembership(table, lastRow, epoch);
1796
+ if (lastRow === 0) for (const directory of table.activeDirectories) directory.delete(table);
2126
1797
  if (row === lastRow) {
2127
1798
  table.size = lastRow;
2128
1799
  return null;
@@ -2150,9 +1821,15 @@ function growTable(table, targetCapacity) {
2150
1821
  componentStorage.fields = fields;
2151
1822
  componentStorage.epochs = growComponentEpochColumns(componentStorage.epochs, capacity);
2152
1823
  }
1824
+ const membership = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
1825
+ membership.set(table.membership);
1826
+ table.membership = membership;
2153
1827
  table.capacity = capacity;
2154
1828
  table.version += 1;
2155
1829
  }
1830
+ function markTableMembership(table, row, epoch) {
1831
+ table.membership[Math.floor(row / PROJECTION_BLOCK_SIZE)] = epoch;
1832
+ }
2156
1833
 
2157
1834
  // src/storage/archetype-graph.ts
2158
1835
  function createArchetypeGraph(shared = false) {
@@ -2161,6 +1838,8 @@ function createArchetypeGraph(shared = false) {
2161
1838
  dedupByKey: /* @__PURE__ */ new Map(),
2162
1839
  generation: 0,
2163
1840
  tables: [],
1841
+ activeTables: /* @__PURE__ */ new Set(),
1842
+ activeTablesByComponent: /* @__PURE__ */ new Map(),
2164
1843
  tableDedupByKey: /* @__PURE__ */ new Map(),
2165
1844
  tableGeneration: 0,
2166
1845
  sparseTags: /* @__PURE__ */ new Map(),
@@ -2179,7 +1858,17 @@ function getOrCreateTable(graph, components) {
2179
1858
  const key = tableKey(tableComponents.map((component) => componentId(component)));
2180
1859
  const existingId = graph.tableDedupByKey.get(key);
2181
1860
  if (existingId !== void 0) return getTable(graph, existingId);
2182
- const table = createTable(tableComponents, graph.tables.length, graph.shared);
1861
+ const directories = [graph.activeTables];
1862
+ for (const component of tableComponents) {
1863
+ const id = componentId(component);
1864
+ let directory = graph.activeTablesByComponent.get(id);
1865
+ if (directory === void 0) {
1866
+ directory = /* @__PURE__ */ new Set();
1867
+ graph.activeTablesByComponent.set(id, directory);
1868
+ }
1869
+ directories.push(directory);
1870
+ }
1871
+ const table = createTable(tableComponents, graph.tables.length, graph.shared, directories);
2183
1872
  graph.tables.push(table);
2184
1873
  graph.tableDedupByKey.set(key, table.id);
2185
1874
  graph.tableGeneration += 1;
@@ -2215,164 +1904,523 @@ function getAddEdge(graph, src, componentId2, component) {
2215
1904
  return target2;
2216
1905
  }
2217
1906
  }
2218
- const newIds = [...src.components.map((c) => componentId(c)), componentId2];
2219
- const newComponents = [...src.components, component];
2220
- const target = getOrCreateArchetype(graph, newIds, newComponents);
2221
- src.addEdges.set(componentId2, target.id);
2222
- return target;
1907
+ const newIds = [...src.components.map((c) => componentId(c)), componentId2];
1908
+ const newComponents = [...src.components, component];
1909
+ const target = getOrCreateArchetype(graph, newIds, newComponents);
1910
+ src.addEdges.set(componentId2, target.id);
1911
+ return target;
1912
+ }
1913
+ function getRemoveEdge(graph, src, componentId2) {
1914
+ const cached = src.removeEdges.get(componentId2);
1915
+ if (cached !== void 0) {
1916
+ const target2 = graph.archetypes[cached];
1917
+ if (target2) {
1918
+ return target2;
1919
+ }
1920
+ }
1921
+ const newIds = src.components.map((c) => componentId(c)).filter((id) => id !== componentId2);
1922
+ const newComponents = src.components.filter((c) => componentId(c) !== componentId2);
1923
+ const target = getOrCreateArchetype(graph, newIds, newComponents);
1924
+ src.removeEdges.set(componentId2, target.id);
1925
+ return target;
1926
+ }
1927
+
1928
+ // src/storage/change-detection.ts
1929
+ var PROJECTION_BLOCK_SIZE = 256;
1930
+ var INITIAL_SPARSE_CAPACITY = 64;
1931
+ function createComponentEpochColumns(capacity) {
1932
+ return {
1933
+ added: new Float64Array(capacity),
1934
+ changed: new Float64Array(capacity),
1935
+ blocks: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE))
1936
+ };
1937
+ }
1938
+ function growComponentEpochColumns(columns, capacity) {
1939
+ const added = new Float64Array(capacity);
1940
+ const changed = new Float64Array(capacity);
1941
+ added.set(columns.added);
1942
+ changed.set(columns.changed);
1943
+ const blocks = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
1944
+ blocks.set(columns.blocks);
1945
+ return { added, changed, blocks };
1946
+ }
1947
+ function copyComponentEpoch(source, sourceRow, target, targetRow) {
1948
+ target.added[targetRow] = source.added[sourceRow] ?? 0;
1949
+ target.changed[targetRow] = source.changed[sourceRow] ?? 0;
1950
+ const block = Math.floor(targetRow / PROJECTION_BLOCK_SIZE);
1951
+ target.blocks[block] = Math.max(target.blocks[block] ?? 0, target.changed[targetRow] ?? 0);
1952
+ }
1953
+ function createSparseTagSet(component) {
1954
+ const sparse = new Int32Array(INITIAL_SPARSE_CAPACITY);
1955
+ sparse.fill(-1);
1956
+ return {
1957
+ component,
1958
+ sparse,
1959
+ dense: new Uint32Array(INITIAL_SPARSE_CAPACITY),
1960
+ added: new Float64Array(INITIAL_SPARSE_CAPACITY),
1961
+ changed: new Float64Array(INITIAL_SPARSE_CAPACITY),
1962
+ size: 0
1963
+ };
1964
+ }
1965
+ function sparseTagIndex(set, entity) {
1966
+ const denseIndex = set.sparse[entityIndex(entity)] ?? -1;
1967
+ return denseIndex >= 0 && set.dense[denseIndex] === entity ? denseIndex : -1;
1968
+ }
1969
+ function insertSparseTag(set, entity, epoch) {
1970
+ const present = sparseTagIndex(set, entity);
1971
+ if (present >= 0) {
1972
+ set.changed[present] = epoch;
1973
+ return present;
1974
+ }
1975
+ growSparseSlots(set, entityIndex(entity) + 1);
1976
+ if (set.size === set.dense.length) growSparseDense(set, set.size + 1);
1977
+ const denseIndex = set.size;
1978
+ set.dense[denseIndex] = entity;
1979
+ set.added[denseIndex] = epoch;
1980
+ set.changed[denseIndex] = epoch;
1981
+ set.sparse[entityIndex(entity)] = denseIndex;
1982
+ set.size += 1;
1983
+ return denseIndex;
1984
+ }
1985
+ function removeSparseTag(set, entity) {
1986
+ const denseIndex = sparseTagIndex(set, entity);
1987
+ if (denseIndex < 0) return false;
1988
+ const lastIndex = set.size - 1;
1989
+ set.sparse[entityIndex(entity)] = -1;
1990
+ if (denseIndex !== lastIndex) {
1991
+ const movedEntity = set.dense[lastIndex];
1992
+ set.dense[denseIndex] = movedEntity;
1993
+ set.added[denseIndex] = set.added[lastIndex] ?? 0;
1994
+ set.changed[denseIndex] = set.changed[lastIndex] ?? 0;
1995
+ set.sparse[entityIndex(movedEntity)] = denseIndex;
1996
+ }
1997
+ set.size = lastIndex;
1998
+ return true;
1999
+ }
2000
+ function growSparseSlots(set, targetCapacity) {
2001
+ if (targetCapacity <= set.sparse.length) return;
2002
+ let capacity = set.sparse.length;
2003
+ while (capacity < targetCapacity) capacity *= 2;
2004
+ const sparse = new Int32Array(capacity);
2005
+ sparse.fill(-1);
2006
+ sparse.set(set.sparse);
2007
+ set.sparse = sparse;
2008
+ }
2009
+ function growSparseDense(set, targetCapacity) {
2010
+ let capacity = set.dense.length;
2011
+ while (capacity < targetCapacity) capacity *= 2;
2012
+ const dense = new Uint32Array(capacity);
2013
+ dense.set(set.dense);
2014
+ set.dense = dense;
2015
+ const added = new Float64Array(capacity);
2016
+ added.set(set.added);
2017
+ set.added = added;
2018
+ const changed = new Float64Array(capacity);
2019
+ changed.set(set.changed);
2020
+ set.changed = changed;
2021
+ }
2022
+ function readComponentChange(graph, location, entity, componentId2) {
2023
+ const sparseSet = graph.sparseTags.get(componentId2);
2024
+ if (sparseSet !== void 0) {
2025
+ const denseIndex = sparseTagIndex(sparseSet, entity);
2026
+ if (denseIndex < 0) return void 0;
2027
+ return {
2028
+ added: sparseSet.added[denseIndex] ?? 0,
2029
+ changed: sparseSet.changed[denseIndex] ?? 0
2030
+ };
2031
+ }
2032
+ const archetype = graph.archetypes[location.archetypeId];
2033
+ if (archetype === void 0) return void 0;
2034
+ const epochs = graph.tables[archetype.tableId]?.storage.get(componentId2)?.epochs;
2035
+ if (epochs === void 0) return void 0;
2036
+ const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
2037
+ return {
2038
+ added: epochs.added[tableRow2] ?? 0,
2039
+ changed: epochs.changed[tableRow2] ?? 0
2040
+ };
2041
+ }
2042
+ function markComponentsAdded(graph, location, entity, componentIds, epoch) {
2043
+ const archetype = graph.archetypes[location.archetypeId];
2044
+ const table = archetype === void 0 ? void 0 : graph.tables[archetype.tableId];
2045
+ const tableRow2 = archetype?.rows[location.archetypeRow] ?? -1;
2046
+ for (const componentId2 of componentIds) {
2047
+ const component = archetype?.components.find(
2048
+ (candidate) => componentId(candidate) === componentId2
2049
+ );
2050
+ if (component?.storage === "sparse") {
2051
+ insertSparseTag(getOrCreateSparseTagSet(graph, component), entity, epoch);
2052
+ continue;
2053
+ }
2054
+ const epochs = table?.storage.get(componentId2)?.epochs;
2055
+ if (epochs === void 0) continue;
2056
+ epochs.added[tableRow2] = epoch;
2057
+ publishComponentRange(epochs, tableRow2, 1, epoch);
2058
+ }
2059
+ }
2060
+ function markComponentChanged(graph, location, entity, componentId2, epoch) {
2061
+ const sparseSet = graph.sparseTags.get(componentId2);
2062
+ if (sparseSet !== void 0) {
2063
+ const denseIndex = sparseTagIndex(sparseSet, entity);
2064
+ if (denseIndex >= 0) sparseSet.changed[denseIndex] = epoch();
2065
+ return;
2066
+ }
2067
+ const archetype = graph.archetypes[location.archetypeId];
2068
+ if (archetype === void 0) return;
2069
+ const epochs = graph.tables[archetype.tableId]?.storage.get(componentId2)?.epochs;
2070
+ if (epochs === void 0) return;
2071
+ const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
2072
+ publishComponentRange(epochs, tableRow2, 1, epoch());
2073
+ }
2074
+ function publishComponentRange(columns, start, count, epoch) {
2075
+ if (count === 0) return;
2076
+ if (count === 1) {
2077
+ columns.changed[start] = epoch;
2078
+ columns.blocks[Math.floor(start / PROJECTION_BLOCK_SIZE)] = epoch;
2079
+ return;
2080
+ }
2081
+ columns.changed.fill(epoch, start, start + count);
2082
+ columns.blocks.fill(
2083
+ epoch,
2084
+ Math.floor(start / PROJECTION_BLOCK_SIZE),
2085
+ Math.ceil((start + count) / PROJECTION_BLOCK_SIZE)
2086
+ );
2087
+ }
2088
+ var SIZE_CLASSES = Object.freeze([
2089
+ 16,
2090
+ 64,
2091
+ 256,
2092
+ 1024,
2093
+ 4096,
2094
+ 16384,
2095
+ 65536,
2096
+ 262144
2097
+ ]);
2098
+ var HAS_TRANSFER2 = typeof ArrayBuffer.prototype.transfer === "function";
2099
+ function bucketIndex(byteLength) {
2100
+ if (byteLength === 0) return -1;
2101
+ for (let i = 0; i < SIZE_CLASSES.length; i++) {
2102
+ const b = SIZE_CLASSES[i];
2103
+ if (b !== void 0 && byteLength <= b) return i;
2104
+ }
2105
+ return SIZE_CLASSES.length;
2106
+ }
2107
+ var BufferPool = class {
2108
+ slots = /* @__PURE__ */ new Map();
2109
+ /**
2110
+ * Per-bucket free-lists: `freeBuckets[i]` is a LIFO stack of slot ids
2111
+ * whose backing buffer is parked on bucket `i`. Released slots stay on
2112
+ * their original bucket's free-list - v1 never moves a slot between
2113
+ * buckets on release (D-7 no trim).
2114
+ */
2115
+ freeBuckets = SIZE_CLASSES.map(() => []);
2116
+ nextId = 1;
2117
+ /**
2118
+ * Allocate a managed buffer slot of at least `byteLength` bytes.
2119
+ *
2120
+ * Routes:
2121
+ * - invalid byteLength -> structured out-of-bounds error.
2122
+ * - byteLength == 0 -> ok({ id, view: zero-length Uint8Array }) (no bucket).
2123
+ * - byteLength <= 262144 -> ok({ id, view }), bucket = smallest >= byteLength.
2124
+ * - byteLength > 262144 -> a dedicated allocation; allocation failure is structured.
2125
+ *
2126
+ * D-5: size classes are radix-4 (16 / 64 / 256 / 1K / 4K / 16K / 64K / 256K).
2127
+ * Free-list pop reuses the most recently released slot id at the same bucket;
2128
+ * miss falls through to a fresh allocation.
2129
+ */
2130
+ alloc(byteLength) {
2131
+ if (byteLength === 0) {
2132
+ const id2 = this.nextId++;
2133
+ const buffer2 = new ArrayBuffer(0);
2134
+ const view2 = new Uint8Array(buffer2);
2135
+ this.slots.set(id2, { sizeClassIdx: -1, buffer: buffer2, view: view2, byteLength: 0, live: true });
2136
+ return ok({ id: id2, view: view2 });
2137
+ }
2138
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
2139
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
2140
+ }
2141
+ const idx = bucketIndex(byteLength);
2142
+ if (idx === SIZE_CLASSES.length) {
2143
+ try {
2144
+ const buffer2 = new ArrayBuffer(byteLength);
2145
+ const view2 = new Uint8Array(buffer2);
2146
+ const id2 = this.nextId++;
2147
+ this.slots.set(id2, { sizeClassIdx: idx, buffer: buffer2, view: view2, byteLength, live: true });
2148
+ return ok({ id: id2, view: view2 });
2149
+ } catch {
2150
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
2151
+ }
2152
+ }
2153
+ const bucketBytes = SIZE_CLASSES[idx];
2154
+ if (bucketBytes === void 0) {
2155
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
2156
+ }
2157
+ const free = this.freeBuckets[idx];
2158
+ if (free !== void 0 && free.length > 0) {
2159
+ const id2 = free.pop();
2160
+ const slot = this.slots.get(id2);
2161
+ if (slot === void 0) {
2162
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
2163
+ }
2164
+ slot.byteLength = byteLength;
2165
+ slot.view = new Uint8Array(slot.buffer, 0, byteLength);
2166
+ slot.live = true;
2167
+ new Uint8Array(slot.buffer).fill(0);
2168
+ return ok({ id: id2, view: slot.view });
2169
+ }
2170
+ const id = this.nextId++;
2171
+ const buffer = new ArrayBuffer(bucketBytes);
2172
+ const view = new Uint8Array(buffer, 0, byteLength);
2173
+ this.slots.set(id, {
2174
+ sizeClassIdx: idx,
2175
+ buffer,
2176
+ view,
2177
+ byteLength,
2178
+ live: true
2179
+ });
2180
+ return ok({ id, view });
2181
+ }
2182
+ /**
2183
+ * Grow slot `id` to `newBytes`. Returns the post-grow Uint8Array view.
2184
+ *
2185
+ * Routes (D-6 / D-7):
2186
+ * - newBytes < current -> err(managed-buffer-shrink-not-supported).
2187
+ * - newBytes == current -> ok(current view) (no-op, identity preserved).
2188
+ * - newBytes > current && same bucket -> ok(re-sliced view) (no transfer).
2189
+ * - newBytes > current && cross bucket -> ok(new view) backed by a fresh
2190
+ * bucket buffer; the prior ArrayBuffer is detached via transfer (ES2024)
2191
+ * or replaced via allocate-and-copy fallback. Old `view`s captured by
2192
+ * the caller become detached / orphaned - callers must use `pool.view(id)`
2193
+ * after grow to read the refreshed view (the `release` loop refreshes
2194
+ * automatically).
2195
+ * - newBytes beyond the last pooled class -> dedicated allocation.
2196
+ */
2197
+ grow(id, newBytes) {
2198
+ const slot = this.slots.get(id);
2199
+ if (slot === void 0) {
2200
+ return err(new ManagedBufferOutOfBoundsError(newBytes, 0));
2201
+ }
2202
+ if (newBytes < slot.byteLength) {
2203
+ return err(new ManagedBufferShrinkNotSupportedError(newBytes, slot.byteLength));
2204
+ }
2205
+ if (newBytes === slot.byteLength) {
2206
+ return ok(slot.view);
2207
+ }
2208
+ if (!Number.isSafeInteger(newBytes) || newBytes < 0) {
2209
+ return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
2210
+ }
2211
+ const newIdx = bucketIndex(newBytes);
2212
+ if (newBytes <= slot.buffer.byteLength) {
2213
+ slot.byteLength = newBytes;
2214
+ slot.view = new Uint8Array(slot.buffer, 0, newBytes);
2215
+ return ok(slot.view);
2216
+ }
2217
+ const newBucketBytes = SIZE_CLASSES[newIdx] ?? Math.max(newBytes, slot.buffer.byteLength * 2);
2218
+ const oldByteLength = slot.byteLength;
2219
+ let nextBuffer;
2220
+ try {
2221
+ if (HAS_TRANSFER2) {
2222
+ nextBuffer = slot.buffer.transfer(newBucketBytes);
2223
+ } else {
2224
+ nextBuffer = new ArrayBuffer(newBucketBytes);
2225
+ new Uint8Array(nextBuffer).set(new Uint8Array(slot.buffer, 0, oldByteLength));
2226
+ }
2227
+ } catch {
2228
+ return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
2229
+ }
2230
+ slot.sizeClassIdx = newIdx;
2231
+ slot.buffer = nextBuffer;
2232
+ slot.byteLength = newBytes;
2233
+ slot.view = new Uint8Array(nextBuffer, 0, newBytes);
2234
+ return ok(slot.view);
2235
+ }
2236
+ /**
2237
+ * Release slot `id` back to its bucket's free-list. Releasing an unknown id
2238
+ * is a no-op - World's release loop drives this and idempotency keeps the
2239
+ * despawn chain free of bookkeeping noise. Bucket free-lists are NEVER
2240
+ * trimmed in v1 (D-7 no trim).
2241
+ */
2242
+ release(id) {
2243
+ const slot = this.slots.get(id);
2244
+ if (slot === void 0) return ok(void 0);
2245
+ if (!slot.live) return ok(void 0);
2246
+ slot.live = false;
2247
+ if (slot.sizeClassIdx >= 0 && slot.sizeClassIdx < SIZE_CLASSES.length) {
2248
+ const bucket = this.freeBuckets[slot.sizeClassIdx];
2249
+ if (bucket !== void 0) bucket.push(id);
2250
+ } else {
2251
+ this.slots.delete(id);
2252
+ }
2253
+ return ok(void 0);
2254
+ }
2255
+ /**
2256
+ * Return the live view for slot `id`. Used by World after `grow` to
2257
+ * refresh the column's stored view reference. Returns a zero-length view
2258
+ * for unknown / released ids so callers never crash on use-after-release.
2259
+ */
2260
+ view(id) {
2261
+ const slot = this.slots.get(id);
2262
+ if (slot === void 0 || !slot.live) return new Uint8Array(0);
2263
+ return slot.view;
2264
+ }
2265
+ /**
2266
+ * Return the bucket-rounded byte capacity for slot `id` --- i.e.
2267
+ * `SIZE_CLASSES[slot.sizeClassIdx]`. Used by managed-buffer view callers
2268
+ * (D-4 no-cache: re-queried per accessor). Returns `0` for the zero-length
2269
+ * (`alloc(0)`) slot; returns `0` for unknown / released ids (mirrors
2270
+ * `view(id)` use-after-release semantics).
2271
+ */
2272
+ byteCapacity(id) {
2273
+ const slot = this.slots.get(id);
2274
+ if (slot === void 0 || !slot.live) return 0;
2275
+ if (slot.sizeClassIdx < 0) return 0;
2276
+ return slot.buffer.byteLength;
2277
+ }
2278
+ /**
2279
+ * Reset the logical byteLength of slot `id` to `newByteLength` while
2280
+ * keeping the same bucket allocation (no transfer, no release). Only
2281
+ * legal when `newByteLength <= bucketBytes`; the slot's bucket index
2282
+ * does not move (D-7 v1 forbids cross-bucket shrink). Used by managed-
2283
+ * buffer clear paths so the slot retains its `byteCapacity` while the
2284
+ * live view becomes zero-length. Bytes past the new logical length are
2285
+ * zero-filled defensively.
2286
+ *
2287
+ * Returns `err(ManagedBufferShrinkNotSupportedError)` when called on a
2288
+ * `sizeClassIdx === -1` slot (alloc(0)) with `newByteLength > 0`, or
2289
+ * when `newByteLength` exceeds the bucket capacity.
2290
+ */
2291
+ setLogicalLength(id, newByteLength) {
2292
+ const slot = this.slots.get(id);
2293
+ if (slot === void 0) {
2294
+ return err(new ManagedBufferOutOfBoundsError(newByteLength, 0));
2295
+ }
2296
+ if (slot.sizeClassIdx < 0) {
2297
+ if (newByteLength === 0) {
2298
+ slot.byteLength = 0;
2299
+ slot.view = new Uint8Array(slot.buffer);
2300
+ return ok(slot.view);
2301
+ }
2302
+ return err(new ManagedBufferOutOfBoundsError(newByteLength, 0));
2303
+ }
2304
+ const bucketBytes = slot.buffer.byteLength;
2305
+ if (newByteLength > bucketBytes) {
2306
+ return err(new ManagedBufferOutOfBoundsError(newByteLength, bucketBytes));
2307
+ }
2308
+ if (newByteLength < slot.byteLength) {
2309
+ new Uint8Array(slot.buffer, newByteLength, slot.byteLength - newByteLength).fill(0);
2310
+ }
2311
+ slot.byteLength = newByteLength;
2312
+ slot.view = new Uint8Array(slot.buffer, 0, newByteLength);
2313
+ return ok(slot.view);
2314
+ }
2315
+ /** @internal Diagnostic count of live slots. Exposed for tests + inspector. */
2316
+ _liveCount() {
2317
+ let n = 0;
2318
+ for (const s of this.slots.values()) if (s.live) n += 1;
2319
+ return n;
2320
+ }
2321
+ };
2322
+
2323
+ // src/component-default-fallback.ts
2324
+ function typeDefault(fieldType) {
2325
+ if (fieldType === "bool") return false;
2326
+ if (fieldType === "entity") return ENTITY_NULL_RAW;
2327
+ if (fieldType === "array<entity>") return [];
2328
+ return 0;
2329
+ }
2330
+ function fillComponentDefaults(token, raw) {
2331
+ const schema = componentSchema(token);
2332
+ const layer2 = componentDefinition(token).defaults;
2333
+ const out = /* @__PURE__ */ Object.create(null);
2334
+ const rawObj = raw ?? void 0;
2335
+ for (const fieldName of Object.keys(schema)) {
2336
+ const fieldType = schema[fieldName];
2337
+ if (fieldType === void 0) continue;
2338
+ if (rawObj !== void 0 && fieldName in rawObj) {
2339
+ out[fieldName] = rawObj[fieldName];
2340
+ continue;
2341
+ }
2342
+ if (layer2 !== void 0 && fieldName in layer2) {
2343
+ out[fieldName] = layer2[fieldName];
2344
+ continue;
2345
+ }
2346
+ out[fieldName] = typeDefault(fieldType);
2347
+ }
2348
+ return out;
2223
2349
  }
2224
- function getRemoveEdge(graph, src, componentId2) {
2225
- const cached = src.removeEdges.get(componentId2);
2226
- if (cached !== void 0) {
2227
- const target2 = graph.archetypes[cached];
2228
- if (target2) {
2229
- return target2;
2350
+ function validateComponentDataKeys(token, raw) {
2351
+ if (raw === void 0) return null;
2352
+ const schema = componentSchema(token);
2353
+ const rawObj = raw;
2354
+ for (const fieldName of Object.keys(rawObj)) {
2355
+ if (!(fieldName in schema)) {
2356
+ return new SpawnDataUnknownFieldError(token.name, fieldName, Object.keys(schema));
2230
2357
  }
2231
2358
  }
2232
- const newIds = src.components.map((c) => componentId(c)).filter((id) => id !== componentId2);
2233
- const newComponents = src.components.filter((c) => componentId(c) !== componentId2);
2234
- const target = getOrCreateArchetype(graph, newIds, newComponents);
2235
- src.removeEdges.set(componentId2, target.id);
2236
- return target;
2359
+ return null;
2237
2360
  }
2238
2361
 
2239
- // src/storage/change-detection.ts
2240
- var INITIAL_SPARSE_CAPACITY = 64;
2241
- function createComponentEpochColumns(capacity) {
2242
- return { added: new Float64Array(capacity), changed: new Float64Array(capacity) };
2243
- }
2244
- function growComponentEpochColumns(columns, capacity) {
2245
- const added = new Float64Array(capacity);
2246
- const changed = new Float64Array(capacity);
2247
- added.set(columns.added);
2248
- changed.set(columns.changed);
2249
- return { added, changed };
2250
- }
2251
- function copyComponentEpoch(source, sourceRow, target, targetRow) {
2252
- target.added[targetRow] = source.added[sourceRow] ?? 0;
2253
- target.changed[targetRow] = source.changed[sourceRow] ?? 0;
2254
- }
2255
- function createSparseTagSet(component) {
2256
- const sparse = new Int32Array(INITIAL_SPARSE_CAPACITY);
2257
- sparse.fill(-1);
2258
- return {
2259
- component,
2260
- sparse,
2261
- dense: new Uint32Array(INITIAL_SPARSE_CAPACITY),
2262
- added: new Float64Array(INITIAL_SPARSE_CAPACITY),
2263
- changed: new Float64Array(INITIAL_SPARSE_CAPACITY),
2264
- size: 0
2265
- };
2266
- }
2267
- function sparseTagIndex(set, entity) {
2268
- const denseIndex = set.sparse[entityIndex(entity)] ?? -1;
2269
- return denseIndex >= 0 && set.dense[denseIndex] === entity ? denseIndex : -1;
2270
- }
2271
- function insertSparseTag(set, entity, epoch) {
2272
- const present = sparseTagIndex(set, entity);
2273
- if (present >= 0) {
2274
- set.changed[present] = epoch;
2275
- return present;
2276
- }
2277
- growSparseSlots(set, entityIndex(entity) + 1);
2278
- if (set.size === set.dense.length) growSparseDense(set, set.size + 1);
2279
- const denseIndex = set.size;
2280
- set.dense[denseIndex] = entity;
2281
- set.added[denseIndex] = epoch;
2282
- set.changed[denseIndex] = epoch;
2283
- set.sparse[entityIndex(entity)] = denseIndex;
2284
- set.size += 1;
2285
- return denseIndex;
2362
+ // src/component-value-validate.ts
2363
+ function isNumericHandle(v) {
2364
+ return typeof v === "number";
2286
2365
  }
2287
- function removeSparseTag(set, entity) {
2288
- const denseIndex = sparseTagIndex(set, entity);
2289
- if (denseIndex < 0) return false;
2290
- const lastIndex = set.size - 1;
2291
- set.sparse[entityIndex(entity)] = -1;
2292
- if (denseIndex !== lastIndex) {
2293
- const movedEntity = set.dense[lastIndex];
2294
- set.dense[denseIndex] = movedEntity;
2295
- set.added[denseIndex] = set.added[lastIndex] ?? 0;
2296
- set.changed[denseIndex] = set.changed[lastIndex] ?? 0;
2297
- set.sparse[entityIndex(movedEntity)] = denseIndex;
2298
- }
2299
- set.size = lastIndex;
2300
- return true;
2366
+ function isSharedScalarType(fieldType) {
2367
+ return fieldType.startsWith("shared<") && fieldType.endsWith(">");
2301
2368
  }
2302
- function growSparseSlots(set, targetCapacity) {
2303
- if (targetCapacity <= set.sparse.length) return;
2304
- let capacity = set.sparse.length;
2305
- while (capacity < targetCapacity) capacity *= 2;
2306
- const sparse = new Int32Array(capacity);
2307
- sparse.fill(-1);
2308
- sparse.set(set.sparse);
2309
- set.sparse = sparse;
2369
+ function isSharedArrayType(fieldType) {
2370
+ if (!fieldType.startsWith("array<") || !fieldType.endsWith(">")) return false;
2371
+ const inner = fieldType.slice(6, -1);
2372
+ const head = inner.indexOf(",") === -1 ? inner : inner.slice(0, inner.indexOf(",")).trim();
2373
+ return head.startsWith("shared<") && head.endsWith(">");
2310
2374
  }
2311
- function growSparseDense(set, targetCapacity) {
2312
- let capacity = set.dense.length;
2313
- while (capacity < targetCapacity) capacity *= 2;
2314
- const dense = new Uint32Array(capacity);
2315
- dense.set(set.dense);
2316
- set.dense = dense;
2317
- const added = new Float64Array(capacity);
2318
- added.set(set.added);
2319
- set.added = added;
2320
- const changed = new Float64Array(capacity);
2321
- changed.set(set.changed);
2322
- set.changed = changed;
2375
+ function isArrayPayload(value) {
2376
+ if (Array.isArray(value)) return true;
2377
+ return ArrayBuffer.isView(value) && typeof value.length === "number";
2323
2378
  }
2324
- function readComponentChange(graph, location, entity, componentId2) {
2325
- const sparseSet = graph.sparseTags.get(componentId2);
2326
- if (sparseSet !== void 0) {
2327
- const denseIndex = sparseTagIndex(sparseSet, entity);
2328
- if (denseIndex < 0) return void 0;
2329
- return {
2330
- added: sparseSet.added[denseIndex] ?? 0,
2331
- changed: sparseSet.changed[denseIndex] ?? 0
2332
- };
2379
+ function validateManagedArrayValues(token, raw) {
2380
+ if (raw === void 0) return null;
2381
+ const fields = componentDefinition(token).fields;
2382
+ const rawObj = raw;
2383
+ for (const fieldName of Object.keys(rawObj)) {
2384
+ const reflection = fields[fieldName];
2385
+ if (reflection?.arrayMeta === void 0) continue;
2386
+ const value = rawObj[fieldName];
2387
+ if (value === 0) continue;
2388
+ if (value === void 0 || value === null || isArrayPayload(value)) continue;
2389
+ return new ManagedArrayInvalidValueError(token.name, fieldName, reflection.type, value);
2333
2390
  }
2334
- const archetype = graph.archetypes[location.archetypeId];
2335
- if (archetype === void 0) return void 0;
2336
- const epochs = graph.tables[archetype.tableId]?.storage.get(componentId2)?.epochs;
2337
- if (epochs === void 0) return void 0;
2338
- const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
2339
- return {
2340
- added: epochs.added[tableRow2] ?? 0,
2341
- changed: epochs.changed[tableRow2] ?? 0
2342
- };
2391
+ return null;
2343
2392
  }
2344
- function markComponentsAdded(graph, location, entity, componentIds, epoch) {
2345
- const archetype = graph.archetypes[location.archetypeId];
2346
- const table = archetype === void 0 ? void 0 : graph.tables[archetype.tableId];
2347
- const tableRow2 = archetype?.rows[location.archetypeRow] ?? -1;
2348
- for (const componentId2 of componentIds) {
2349
- const component = archetype?.components.find(
2350
- (candidate) => componentId(candidate) === componentId2
2351
- );
2352
- if (component?.storage === "sparse") {
2353
- insertSparseTag(getOrCreateSparseTagSet(graph, component), entity, epoch);
2354
- continue;
2393
+ function validateSharedFieldValues(token, raw) {
2394
+ if (raw === void 0) return null;
2395
+ const schema = componentSchema(token);
2396
+ const rawObj = raw;
2397
+ for (const fieldName of Object.keys(rawObj)) {
2398
+ const fieldType = schema[fieldName];
2399
+ if (fieldType === void 0) continue;
2400
+ const value = rawObj[fieldName];
2401
+ if (value === void 0 || value === null) continue;
2402
+ if (isSharedScalarType(fieldType)) {
2403
+ if (!isNumericHandle(value)) {
2404
+ return new SharedFieldInvalidValueError(token.name, fieldName, fieldType, value);
2405
+ }
2406
+ } else if (isSharedArrayType(fieldType)) {
2407
+ if (!Array.isArray(value)) continue;
2408
+ for (let i = 0; i < value.length; i++) {
2409
+ const el = value[i];
2410
+ if (el === void 0 || el === null) continue;
2411
+ if (!isNumericHandle(el)) {
2412
+ return new SharedFieldInvalidValueError(token.name, fieldName, fieldType, el, i);
2413
+ }
2414
+ }
2355
2415
  }
2356
- const epochs = table?.storage.get(componentId2)?.epochs;
2357
- if (epochs === void 0) continue;
2358
- epochs.added[tableRow2] = epoch;
2359
- epochs.changed[tableRow2] = epoch;
2360
- }
2361
- }
2362
- function markComponentChanged(graph, location, entity, componentId2, epoch) {
2363
- const sparseSet = graph.sparseTags.get(componentId2);
2364
- if (sparseSet !== void 0) {
2365
- const denseIndex = sparseTagIndex(sparseSet, entity);
2366
- if (denseIndex >= 0) sparseSet.changed[denseIndex] = epoch();
2367
- return;
2368
2416
  }
2369
- const archetype = graph.archetypes[location.archetypeId];
2370
- if (archetype === void 0) return;
2371
- const epochs = graph.tables[archetype.tableId]?.storage.get(componentId2)?.epochs;
2372
- if (epochs === void 0) return;
2373
- const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
2374
- epochs.changed[tableRow2] = epoch();
2417
+ return null;
2375
2418
  }
2419
+
2420
+ // src/internal.ts
2421
+ var DERIVED_WRITER = /* @__PURE__ */ Symbol.for(
2422
+ "forgeax.ecs.query.derivedWriter"
2423
+ );
2376
2424
  function createDerivedRangeWriter(world, component, source) {
2377
2425
  let boundEpoch = -1;
2378
2426
  let bindingTables = [];
@@ -2686,14 +2734,11 @@ var QueryRowFacade = class _QueryRowFacade {
2686
2734
  return new _QueryRowFacade(this.world).bind(this.entity, this.archetype);
2687
2735
  }
2688
2736
  has(component) {
2689
- return this.archetype?.components.some(
2690
- (candidate) => componentId(candidate) === componentId(component)
2691
- ) === true;
2737
+ const id = componentId(component);
2738
+ return this.archetype?.components.some((candidate) => componentId(candidate) === id) === true;
2692
2739
  }
2693
2740
  get(component) {
2694
- if (!this.archetype?.components.some(
2695
- (candidate) => componentId(candidate) === componentId(component)
2696
- )) {
2741
+ if (!this.has(component)) {
2697
2742
  return void 0;
2698
2743
  }
2699
2744
  const result = this.world[worldInternal].getQueryRow(this.entity, component);
@@ -2805,8 +2850,9 @@ var ExecutableQuery = class {
2805
2850
  const structureEpoch = this.world[worldInternal].getStructureEpoch();
2806
2851
  const upperBound = this.world[worldInternal].getMutationEpoch();
2807
2852
  const row = new QueryRowFacade(this.world);
2808
- let archetypeIndex = 0;
2809
- let tableIndex = 0;
2853
+ const unchanged = this.hasUnchangedInput();
2854
+ let archetypeIndex = unchanged ? this.matchedArchetypes.length : 0;
2855
+ let tableIndex = unchanged ? this.matchedTables.length : 0;
2810
2856
  let rowIndex = 0;
2811
2857
  let finished = false;
2812
2858
  const close = (commit) => {
@@ -2874,7 +2920,7 @@ var ExecutableQuery = class {
2874
2920
  at(entity) {
2875
2921
  const archetype = this.world[worldInternal].getEntityArchetype(entity);
2876
2922
  if (archetype === void 0 || !this.archetypeMatches(archetype)) return void 0;
2877
- return new QueryRowFacade(this.world).bind(entity, archetype).snapshot();
2923
+ return new QueryRowFacade(this.world).bind(entity, archetype);
2878
2924
  }
2879
2925
  spans() {
2880
2926
  const reason = this.spanUnavailableReason();
@@ -2886,7 +2932,8 @@ var ExecutableQuery = class {
2886
2932
  query.refreshMatches();
2887
2933
  const structureEpoch = query.world[worldInternal].getStructureEpoch();
2888
2934
  const upperBound = query.world[worldInternal].getMutationEpoch();
2889
- let tableIndex = 0;
2935
+ const filterIds = [...query.compiled.changedIds, ...query.compiled.addedIds];
2936
+ let tableIndex = query.hasUnchangedInput() ? query.matchedTables.length : 0;
2890
2937
  let rowIndex = 0;
2891
2938
  let finished = false;
2892
2939
  const close = (commit) => {
@@ -2921,7 +2968,11 @@ var ExecutableQuery = class {
2921
2968
  };
2922
2969
  }
2923
2970
  while (rowIndex < table.size && !query.denseChangeMatches(table, rowIndex, upperBound)) {
2924
- rowIndex += 1;
2971
+ const block = Math.floor(rowIndex / PROJECTION_BLOCK_SIZE);
2972
+ const unchanged = filterIds.some(
2973
+ (id) => (table.storage.get(id)?.epochs.blocks[block] ?? 0) <= query.lastObservedEpoch
2974
+ );
2975
+ rowIndex = unchanged ? (block + 1) * PROJECTION_BLOCK_SIZE : rowIndex + 1;
2925
2976
  }
2926
2977
  if (rowIndex >= table.size) {
2927
2978
  tableIndex += 1;
@@ -3009,6 +3060,10 @@ var ExecutableQuery = class {
3009
3060
  }
3010
3061
  };
3011
3062
  }
3063
+ hasUnchangedInput() {
3064
+ const epochs = this.world[worldInternal].getComponentMutationEpochs();
3065
+ return this.compiled.changedIds.some((id) => (epochs[id] ?? 0) <= this.lastObservedEpoch);
3066
+ }
3012
3067
  beginIteration() {
3013
3068
  if (this.active) throw new QueryIterationActiveError();
3014
3069
  this.active = true;
@@ -3925,7 +3980,6 @@ function runSchedule(schedule, world, selectedNames, commandsBySystem = /* @__PU
3925
3980
  if (!completed || finalDrain) abortOutstandingCommands(commandsBySystem);
3926
3981
  }
3927
3982
  }
3928
- var SHARED_REF_RELEASE_EVIDENCE_CAPACITY = 4096;
3929
3983
  var SharedRefStore = class {
3930
3984
  payloads = /* @__PURE__ */ new Map();
3931
3985
  refcounts = /* @__PURE__ */ new Map();
@@ -3933,10 +3987,7 @@ var SharedRefStore = class {
3933
3987
  internedByTarget = /* @__PURE__ */ new Map();
3934
3988
  internedKeys = /* @__PURE__ */ new Map();
3935
3989
  nextSlot = BUILTIN_BASE;
3936
- mutationEpoch = 0;
3937
3990
  /** Latest published mutation epoch per live handle; not an event journal. */
3938
- mutationEpochs = /* @__PURE__ */ new Map();
3939
- releaseJournal = [];
3940
3991
  /**
3941
3992
  * Generation table indexed by slot (D-6). Each entry tracks the current
3942
3993
  * generation for the slot — written to during alloc (welded into the
@@ -4032,32 +4083,6 @@ var SharedRefStore = class {
4032
4083
  * projections of shared payload data compare the monotonic epoch and
4033
4084
  * explicitly refresh instead of rescanning every payload each frame.
4034
4085
  */
4035
- markChanged(handle) {
4036
- const resolved = this.resolve(handle);
4037
- if (!resolved.ok) return resolved;
4038
- if (this.mutationEpoch >= Number.MAX_SAFE_INTEGER) {
4039
- throw new RangeError("SharedRefStore mutation epoch exhausted");
4040
- }
4041
- this.mutationEpoch += 1;
4042
- this.mutationEpochs.set(unwrapHandle(handle), this.mutationEpoch);
4043
- return ok(void 0);
4044
- }
4045
- /** Current upper bound for explicitly published payload mutations. */
4046
- getMutationEpoch() {
4047
- return this.mutationEpoch;
4048
- }
4049
- /** Read each live handle whose latest published mutation is after `cursor`. */
4050
- readChangesSince(cursor) {
4051
- const records = [];
4052
- for (const [handle, epoch] of this.mutationEpochs) {
4053
- if (epoch > cursor) records.push({ epoch, handle });
4054
- }
4055
- records.sort((left, right) => left.epoch - right.epoch || left.handle - right.handle);
4056
- return {
4057
- cursor: this.mutationEpoch,
4058
- records
4059
- };
4060
- }
4061
4086
  /**
4062
4087
  * Increment the refcount of a live shared handle. Returns
4063
4088
  * `err(shared-ref-released)` when the handle is not live - retain MUST
@@ -4121,7 +4146,6 @@ var SharedRefStore = class {
4121
4146
  }
4122
4147
  this.refcounts.delete(raw);
4123
4148
  this.payloads.delete(raw);
4124
- this.mutationEpochs.delete(raw);
4125
4149
  const generation = storeGen + 1;
4126
4150
  this._generations[slot] = generation;
4127
4151
  if (!isRetiredSlot(generation)) {
@@ -4133,23 +4157,8 @@ var SharedRefStore = class {
4133
4157
  generation,
4134
4158
  evidence: "released"
4135
4159
  });
4136
- this.releaseJournal.push(
4137
- Object.freeze({
4138
- handle: raw,
4139
- refcount: 0,
4140
- generation,
4141
- evidence: "released"
4142
- })
4143
- );
4144
- if (this.releaseJournal.length > SHARED_REF_RELEASE_EVIDENCE_CAPACITY) {
4145
- this.releaseJournal.shift();
4146
- }
4147
4160
  return ok(evidence);
4148
4161
  }
4149
- /** Read bounded release metadata; payload ownership remains with the caller. */
4150
- readReleaseEvidence() {
4151
- return this.releaseJournal;
4152
- }
4153
4162
  /**
4154
4163
  * Return the current refcount for `handle`. Returns 0 for a released
4155
4164
  * (or never-allocated) slot. Primarily a debug + tests entry point;
@@ -4165,47 +4174,6 @@ var SharedRefStore = class {
4165
4174
  return this.payloads.size;
4166
4175
  }
4167
4176
  };
4168
-
4169
- // src/storage/structural-evidence.ts
4170
- var StructuralEvidenceRing = class {
4171
- constructor(capacity = 1024) {
4172
- this.capacity = capacity;
4173
- if (!Number.isSafeInteger(capacity) || capacity <= 0) {
4174
- throw new RangeError("StructuralEvidenceRing capacity must be a positive safe integer");
4175
- }
4176
- this.events = new Array(capacity);
4177
- }
4178
- capacity;
4179
- events;
4180
- nextSequence = 1;
4181
- get cursor() {
4182
- return this.nextSequence - 1;
4183
- }
4184
- append(input) {
4185
- const sequence = this.nextSequence;
4186
- this.nextSequence += 1;
4187
- this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
4188
- return sequence;
4189
- }
4190
- readAfter(cursor) {
4191
- const latest = this.cursor;
4192
- if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
4193
- throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
4194
- }
4195
- const oldestAvailable = Math.max(1, latest - this.capacity + 1);
4196
- if (cursor < oldestAvailable - 1)
4197
- return { status: "overflow", cursor: latest, oldestAvailable };
4198
- const events = [];
4199
- for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
4200
- const event = this.events[(sequence - 1) % this.capacity];
4201
- if (event === void 0 || event.sequence !== sequence) {
4202
- return { status: "overflow", cursor: latest, oldestAvailable };
4203
- }
4204
- events.push(event);
4205
- }
4206
- return { status: "ok", cursor: latest, events };
4207
- }
4208
- };
4209
4177
  var UniqueRefStore = class {
4210
4178
  payloads = /* @__PURE__ */ new Map();
4211
4179
  freeSlots = [];
@@ -5057,7 +5025,6 @@ var World = class {
5057
5025
  /** Per-World shared-ref store; public read-only for direct handle operations. */
5058
5026
  sharedRefs = new SharedRefStore();
5059
5027
  componentMutationEpochs = [];
5060
- structuralEvidence = new StructuralEvidenceRing();
5061
5028
  /** One packed reverse index per relationship source component. */
5062
5029
  relationshipIndexes = /* @__PURE__ */ new Map();
5063
5030
  mutationEpoch = 0;
@@ -5112,7 +5079,6 @@ var World = class {
5112
5079
  getSchedules: () => this.schedules,
5113
5080
  getSharedRefs: () => this.sharedRefs,
5114
5081
  getStructureEpoch: this.getStructureEpoch.bind(this),
5115
- getStructuralEvidence: () => this.structuralEvidence,
5116
5082
  lookupAlive: this.lookupAlive.bind(this),
5117
5083
  markComponentChanged: this.internalmarkComponentChanged.bind(this),
5118
5084
  markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
@@ -5177,9 +5143,6 @@ var World = class {
5177
5143
  }
5178
5144
  return worldScheduleUsesComponent(this, component);
5179
5145
  }
5180
- recordStructuralEvidence(evidence) {
5181
- this.structuralEvidence.append(evidence);
5182
- }
5183
5146
  /** Resolve current logical identity for a packed entity handle. */
5184
5147
  internalgetEntityArchetype(entity) {
5185
5148
  const record = this.records[entityIndex(entity)];
@@ -5208,7 +5171,7 @@ var World = class {
5208
5171
  if (epochs === void 0) {
5209
5172
  throw new Error(`Derived component ${componentId2} is not in the table.`);
5210
5173
  }
5211
- epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
5174
+ publishComponentRange(epochs, rowStart, rowCount, epoch);
5212
5175
  this.componentMutationEpochs[componentId2] = epoch;
5213
5176
  }
5214
5177
  /** Record one successful structural mutation. */
@@ -5247,7 +5210,7 @@ var World = class {
5247
5210
  const epochs = table.storage.get(componentId2)?.epochs;
5248
5211
  if (epochs === void 0 || rowCount === 0) return;
5249
5212
  const epoch = this.internalnextMutationEpoch();
5250
- epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
5213
+ publishComponentRange(epochs, rowStart, rowCount, epoch);
5251
5214
  this.componentMutationEpochs[componentId2] = epoch;
5252
5215
  }
5253
5216
  /** Query facade write after the facade has already marked evidence. */
@@ -5871,11 +5834,6 @@ var World = class {
5871
5834
  if (mirrorAdded) {
5872
5835
  this.internalmarkComponentsAdded(target, [mirrorLocalId]);
5873
5836
  this.advanceStructureEpoch();
5874
- this.recordStructuralEvidence({
5875
- kind: "component-added",
5876
- entity: target,
5877
- componentId: mirrorLocalId
5878
- });
5879
5837
  }
5880
5838
  return ok(void 0);
5881
5839
  }
@@ -6076,12 +6034,7 @@ var World = class {
6076
6034
  data: value
6077
6035
  });
6078
6036
  if (!valuePreflight.ok) return valuePreflight;
6079
- const currentValue = this.readRow(arch, component, row);
6080
- const mergedValue = {
6081
- ...currentValue,
6082
- ...value
6083
- };
6084
- const enumError = validateEnumFieldValues(component, mergedValue, entity);
6037
+ const enumError = validateEnumFieldValues(component, value, entity);
6085
6038
  if (enumError !== null) return err(enumError);
6086
6039
  if (component.storage === "sparse") {
6087
6040
  if (markChanged) this.markComponentChanged(entity, component);
@@ -6456,11 +6409,6 @@ var World = class {
6456
6409
  if (!internal) {
6457
6410
  this.internalmarkComponentsAdded(entity, [componentId(componentData.component)]);
6458
6411
  this.advanceStructureEpoch();
6459
- this.recordStructuralEvidence({
6460
- kind: "component-added",
6461
- entity,
6462
- componentId: localId
6463
- });
6464
6412
  }
6465
6413
  return ok(void 0);
6466
6414
  }
@@ -6542,12 +6490,8 @@ var World = class {
6542
6490
  this.migrateEntity(rec, srcArch, targetArch);
6543
6491
  }
6544
6492
  if (!internal) {
6493
+ this.internalnextMutationEpoch();
6545
6494
  this.advanceStructureEpoch();
6546
- this.recordStructuralEvidence({
6547
- kind: "component-removed",
6548
- entity,
6549
- componentId: localId
6550
- });
6551
6495
  }
6552
6496
  return ok(void 0);
6553
6497
  }
@@ -6597,7 +6541,7 @@ var World = class {
6597
6541
  record.archetypeId = arch.id;
6598
6542
  storageTouched = true;
6599
6543
  const table = this.table(arch);
6600
- const tableRow2 = appendTableRow(table, entity);
6544
+ const tableRow2 = appendTableRow(table, entity, this.mutationEpoch + 1);
6601
6545
  const archetypeRow = appendArchetypeRow(arch, tableRow2);
6602
6546
  record.archetypeRow = archetypeRow;
6603
6547
  for (const cd of componentDatas) {
@@ -6623,7 +6567,6 @@ var World = class {
6623
6567
  ...componentDatas.map((cd) => componentId(cd.component))
6624
6568
  ]);
6625
6569
  this.advanceStructureEpoch();
6626
- this.recordStructuralEvidence({ kind: "spawn", entity });
6627
6570
  return ok(void 0);
6628
6571
  } catch (error) {
6629
6572
  if (storageTouched) this.poisonAfterEntityMutation("World.materializeEntity", error);
@@ -7003,6 +6946,18 @@ var World = class {
7003
6946
  const elementBytes = meta.byteSize;
7004
6947
  const isVariable = arrayMeta.length === void 0;
7005
6948
  const fixedLength = arrayMeta.length ?? 0;
6949
+ if (!isVariable && metaKey !== "shared" && meta.viewCtor !== void 0 && col.view instanceof meta.viewCtor && (raw == null || Array.isArray(raw) || raw instanceof meta.viewCtor)) {
6950
+ const start = row * col.arity;
6951
+ const count = raw == null ? 0 : Math.min(raw.length, col.arity);
6952
+ if (Array.isArray(raw)) {
6953
+ for (let i = 0; i < count; i++)
6954
+ col.view[start + i] = typeof raw[i] === "number" ? raw[i] : 0;
6955
+ } else if (raw != null) {
6956
+ col.view.set(raw.length <= col.arity ? raw : raw.subarray(0, count), start);
6957
+ }
6958
+ col.view.fill(0, start + count, start + col.arity);
6959
+ return;
6960
+ }
7006
6961
  let payloadCount = 0;
7007
6962
  let payloadBytes = null;
7008
6963
  if (raw !== null && raw !== void 0) {
@@ -7141,7 +7096,7 @@ var World = class {
7141
7096
  const srcTable = this.table(srcArch);
7142
7097
  const targetTable = this.table(targetArch);
7143
7098
  const entity = srcTable.storage.get(componentId(Entity))?.fields.get("self")?.view[oldTableRow] ?? 0;
7144
- const newTableRow = appendTableRow(targetTable, entity);
7099
+ const newTableRow = appendTableRow(targetTable, entity, this.mutationEpoch + 1);
7145
7100
  const newArchetypeRow = appendArchetypeRow(targetArch, newTableRow);
7146
7101
  for (const [compId, srcComponentStorage] of srcTable.storage) {
7147
7102
  const srcFieldCols = srcComponentStorage.fields;
@@ -7178,7 +7133,7 @@ var World = class {
7178
7133
  movedRecord.archetypeRow = archetypeSwap.newRow;
7179
7134
  }
7180
7135
  }
7181
- const tableSwap = removeTableRow(srcTable, oldTableRow);
7136
+ const tableSwap = removeTableRow(srcTable, oldTableRow, this.mutationEpoch + 1);
7182
7137
  if (tableSwap !== null) {
7183
7138
  const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
7184
7139
  if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
@@ -7208,6 +7163,7 @@ var World = class {
7208
7163
  }
7209
7164
  record.archetypeId = targetArch.id;
7210
7165
  record.archetypeRow = appendArchetypeRow(targetArch, tableRow2);
7166
+ markTableMembership(table, tableRow2, this.mutationEpoch + 1);
7211
7167
  }
7212
7168
  /**
7213
7169
  * Retire one live entity and any linked-spawn descendants. The complete
@@ -7254,7 +7210,7 @@ var World = class {
7254
7210
  movedRecord.archetypeRow = archetypeSwap.newRow;
7255
7211
  }
7256
7212
  }
7257
- const tableSwap = removeTableRow(table, tableRow2);
7213
+ const tableSwap = removeTableRow(table, tableRow2, this.mutationEpoch + 1);
7258
7214
  if (tableSwap !== null) {
7259
7215
  const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
7260
7216
  if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
@@ -7265,7 +7221,6 @@ var World = class {
7265
7221
  }
7266
7222
  }
7267
7223
  }
7268
- this.recordStructuralEvidence({ kind: "despawn", entity });
7269
7224
  record.archetypeId = -1;
7270
7225
  record.archetypeRow = -1;
7271
7226
  record.generation += 1;
@@ -7274,6 +7229,7 @@ var World = class {
7274
7229
  const childResult = this.despawnEntity(child, true);
7275
7230
  if (!childResult.ok) return childResult;
7276
7231
  }
7232
+ this.internalnextMutationEpoch();
7277
7233
  this.advanceStructureEpoch();
7278
7234
  return ok(void 0);
7279
7235
  } catch (error) {