@forgeax/engine-ecs 0.1.23 → 0.1.25

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 (42) hide show
  1. package/README.md +42 -1
  2. package/dist/buffer-pool.d.ts +4 -5
  3. package/dist/buffer-pool.d.ts.map +1 -1
  4. package/dist/component.d.ts +9 -6
  5. package/dist/component.d.ts.map +1 -1
  6. package/dist/errors/query-and-component-errors.d.ts +1 -1
  7. package/dist/errors/query-and-component-errors.d.ts.map +1 -1
  8. package/dist/externalization/index.mjs.map +1 -1
  9. package/dist/index.mjs +597 -152
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/internal.d.ts +3 -1
  12. package/dist/internal.d.ts.map +1 -1
  13. package/dist/internal.mjs +6 -1
  14. package/dist/internal.mjs.map +1 -1
  15. package/dist/projection/index.mjs.map +1 -1
  16. package/dist/query/derived-range-writer.d.ts +14 -0
  17. package/dist/query/derived-range-writer.d.ts.map +1 -1
  18. package/dist/query/query.d.ts.map +1 -1
  19. package/dist/shared.mjs.map +1 -1
  20. package/dist/world-component-access.d.ts +63 -18
  21. package/dist/world-component-access.d.ts.map +1 -1
  22. package/dist/world-component-storage.d.ts +26 -10
  23. package/dist/world-component-storage.d.ts.map +1 -1
  24. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  25. package/dist/world-internal.d.ts +1 -1
  26. package/dist/world-internal.d.ts.map +1 -1
  27. package/dist/world.d.ts +5 -0
  28. package/dist/world.d.ts.map +1 -1
  29. package/package.json +4 -4
  30. package/src/__tests__/archetype.unit.test.ts +38 -10
  31. package/src/__tests__/relationship-index.test.ts +147 -1
  32. package/src/buffer-pool.ts +47 -74
  33. package/src/component.ts +9 -6
  34. package/src/errors/query-and-component-errors.ts +4 -1
  35. package/src/internal.ts +3 -0
  36. package/src/query/derived-range-writer.ts +111 -0
  37. package/src/query/query.ts +26 -1
  38. package/src/world-component-access.ts +427 -127
  39. package/src/world-component-storage.ts +104 -18
  40. package/src/world-entity-lifecycle.ts +49 -9
  41. package/src/world-internal.ts +5 -0
  42. package/src/world.ts +52 -3
package/dist/index.mjs CHANGED
@@ -2046,6 +2046,7 @@ function createDerivedRangeWriter(world, component, source) {
2046
2046
  let boundEpoch = -1;
2047
2047
  let bindingTables = [];
2048
2048
  let bindings = [];
2049
+ let bindingByTable = /* @__PURE__ */ new Map();
2049
2050
  let runStartBuffers = [];
2050
2051
  let runCountBuffers = [];
2051
2052
  const rebind = () => {
@@ -2055,6 +2056,8 @@ function createDerivedRangeWriter(world, component, source) {
2055
2056
  const nextRunCountBuffers = [];
2056
2057
  for (const table of tables) {
2057
2058
  nextBindings.push({
2059
+ tableId: table.id,
2060
+ entities: table.storage.get(componentId(Entity))?.fields.get("self")?.view ?? new Uint32Array(0),
2058
2061
  read: buildWholeColumnShape(table, source.readComponents),
2059
2062
  write: buildWholeColumnShape(table, source.writeComponents),
2060
2063
  rowCapacity: table.size
@@ -2065,6 +2068,11 @@ function createDerivedRangeWriter(world, component, source) {
2065
2068
  }
2066
2069
  bindingTables = tables;
2067
2070
  bindings = nextBindings;
2071
+ bindingByTable = /* @__PURE__ */ new Map();
2072
+ for (let index = 0; index < nextBindings.length; index += 1) {
2073
+ const binding = nextBindings[index];
2074
+ if (binding !== void 0) bindingByTable.set(binding.tableId, index);
2075
+ }
2068
2076
  runStartBuffers = nextRunStartBuffers;
2069
2077
  runCountBuffers = nextRunCountBuffers;
2070
2078
  boundEpoch = source.structureEpoch();
@@ -2075,6 +2083,82 @@ function createDerivedRangeWriter(world, component, source) {
2075
2083
  if (boundEpoch !== source.structureEpoch()) rebind();
2076
2084
  return bindings;
2077
2085
  },
2086
+ locateEntity(entity, cursor) {
2087
+ if (world.execution.health === "poisoned") return false;
2088
+ if (boundEpoch !== source.structureEpoch()) rebind();
2089
+ const archetype = world[worldInternal].getEntityArchetype(entity);
2090
+ if (archetype === void 0) return false;
2091
+ const bindingIndex = bindingByTable.get(archetype.tableId);
2092
+ if (bindingIndex === void 0) return false;
2093
+ const record = world[worldInternal].getRecords()[entity & 16777215];
2094
+ if (record === void 0 || record.archetypeId !== archetype.id) return false;
2095
+ const row = archetype.rows[record.archetypeRow];
2096
+ const binding = bindings[bindingIndex];
2097
+ if (binding === void 0 || row === void 0 || row < 0 || row >= binding.rowCapacity) {
2098
+ return false;
2099
+ }
2100
+ cursor.bindingIndex = bindingIndex;
2101
+ cursor.row = row;
2102
+ return true;
2103
+ },
2104
+ publishChangedRows(bindingIndex, changed) {
2105
+ if (world.execution.health === "poisoned") {
2106
+ return err(new WorldPoisonedError(world.identity, world.execution.fault));
2107
+ }
2108
+ if (boundEpoch !== source.structureEpoch()) rebind();
2109
+ const binding = bindings[bindingIndex];
2110
+ const table = bindingTables[bindingIndex];
2111
+ const runStarts = runStartBuffers[bindingIndex];
2112
+ const runCounts = runCountBuffers[bindingIndex];
2113
+ if (binding === void 0 || table === void 0 || runStarts === void 0 || runCounts === void 0 || !Number.isSafeInteger(bindingIndex) || bindingIndex < 0 || changed.length < binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
2114
+ return err(new DerivedRangeOutOfBoundsError(0, changed.length, binding?.rowCapacity ?? 0));
2115
+ }
2116
+ let runCount = 0;
2117
+ let runStart = -1;
2118
+ for (let row = 0; row < binding.rowCapacity; row += 1) {
2119
+ if ((changed[row] ?? 0) !== 0) {
2120
+ if (runStart < 0) runStart = row;
2121
+ } else if (runStart >= 0) {
2122
+ runStarts[runCount] = runStart;
2123
+ runCounts[runCount] = row - runStart;
2124
+ runCount += 1;
2125
+ runStart = -1;
2126
+ }
2127
+ }
2128
+ if (runStart >= 0) {
2129
+ runStarts[runCount] = runStart;
2130
+ runCounts[runCount] = binding.rowCapacity - runStart;
2131
+ runCount += 1;
2132
+ }
2133
+ if (runCount === 0) return ok(void 0);
2134
+ const previousEpoch = world[worldInternal].getMutationEpoch();
2135
+ let epoch;
2136
+ try {
2137
+ epoch = world[worldInternal].nextMutationEpoch();
2138
+ const componentIdentifier = componentId(component);
2139
+ for (let index = 0; index < runCount; index += 1) {
2140
+ world[worldInternal].publishDerivedRange(
2141
+ table,
2142
+ componentIdentifier,
2143
+ runStarts[index] ?? 0,
2144
+ runCounts[index] ?? 0,
2145
+ epoch
2146
+ );
2147
+ }
2148
+ changed.fill(0, 0, binding.rowCapacity);
2149
+ return ok(void 0);
2150
+ } catch (cause) {
2151
+ world[worldInternal].restoreMutationEpoch(previousEpoch);
2152
+ world[worldInternal].poisonExecution({
2153
+ code: "shared-kernel-failed",
2154
+ kernelName: `derived-range:${component.name}:publish`,
2155
+ cause,
2156
+ partialWrite: true,
2157
+ retryable: false
2158
+ });
2159
+ return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
2160
+ }
2161
+ },
2078
2162
  writeRange(bindingIndex, base, start, count, kernel, context) {
2079
2163
  if (world.execution.health === "poisoned") {
2080
2164
  return err(new WorldPoisonedError(world.identity, world.execution.fault));
@@ -2288,7 +2372,13 @@ var QueryRowFacade = class _QueryRowFacade {
2288
2372
  mut(component) {
2289
2373
  const current = this.get(component);
2290
2374
  if (current === void 0) throw new Error(`Query row lacks ${component.name}.`);
2291
- this.world[worldInternal].markComponentChanged(this.entity, componentId(component));
2375
+ if (isRelationshipTarget(component)) {
2376
+ throw new RelationshipTargetReadonlyError(component.name, "query row");
2377
+ }
2378
+ const relationshipSource2 = relationshipRole(component)?.kind === "source";
2379
+ if (!relationshipSource2) {
2380
+ this.world[worldInternal].markComponentChanged(this.entity, componentId(component));
2381
+ }
2292
2382
  return new Proxy(current, {
2293
2383
  set: (target, property, value) => {
2294
2384
  if (typeof property !== "string") return false;
@@ -2325,6 +2415,9 @@ var QuerySpanFacade = class {
2325
2415
  );
2326
2416
  }
2327
2417
  mut(component) {
2418
+ if (relationshipRole(component) !== void 0) {
2419
+ throw new RelationshipTargetReadonlyError(component.name, "query span");
2420
+ }
2328
2421
  this.world[worldInternal].markComponentRangeChanged(
2329
2422
  this.table,
2330
2423
  componentId(component),
@@ -2650,6 +2743,9 @@ var ExecutableQuery = class {
2650
2743
  spanUnavailableReason() {
2651
2744
  if (this.compiled.sparseRoute) return "sparse-component";
2652
2745
  if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return "optional-data";
2746
+ if ((this.compiled.descriptor.write ?? []).some((component) => relationshipRole(component))) {
2747
+ return "relationship-component";
2748
+ }
2653
2749
  return void 0;
2654
2750
  }
2655
2751
  };
@@ -3637,6 +3733,52 @@ var ComponentStorage = class {
3637
3733
  const elementCount = countCol?.view[row] ?? 0;
3638
3734
  return reinterpretSlotBytes(liveBytes, arrayMeta.elementType, elementCount);
3639
3735
  }
3736
+ /**
3737
+ * Read the live length of an array field without materialising a typed view.
3738
+ * Relationship consumers use this narrow storage seam for hot reverse-list
3739
+ * walks; the target array remains owned by the ECS column and no snapshot is
3740
+ * created per entity.
3741
+ */
3742
+ readArrayLength(arch, component, row, fieldName) {
3743
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
3744
+ if (fieldCols === void 0) return void 0;
3745
+ const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
3746
+ if (arrayMeta === void 0) return void 0;
3747
+ if (arrayMeta.length !== void 0) return arrayMeta.length;
3748
+ const count = fieldCols.get(arrayCountColumnName(fieldName))?.view[row];
3749
+ return typeof count === "number" ? count : 0;
3750
+ }
3751
+ /**
3752
+ * Read one array element directly from its backing column/BufferPool slot.
3753
+ * The relationship target vocabulary is `array<entity>`, so its hot path
3754
+ * decodes the packed u32 in-place and does not allocate a TypedArray view.
3755
+ */
3756
+ readArrayElement(arch, component, row, fieldName, index) {
3757
+ if (!Number.isSafeInteger(index) || index < 0) return void 0;
3758
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
3759
+ if (fieldCols === void 0) return void 0;
3760
+ const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
3761
+ if (arrayMeta === void 0) return void 0;
3762
+ const length = this.readArrayLength(arch, component, row, fieldName);
3763
+ if (length === void 0 || index >= length) return void 0;
3764
+ const col = fieldCols.get(fieldName);
3765
+ if (col === void 0) return void 0;
3766
+ if (arrayMeta.length !== void 0) {
3767
+ return col.view[row * col.arity + index];
3768
+ }
3769
+ const slotId = col.view[row];
3770
+ const bytes = this.bufferPool.view(slotId);
3771
+ if (arrayMeta.elementType === "entity") {
3772
+ const byteOffset = index * 4;
3773
+ if (byteOffset + 4 > bytes.byteLength) return void 0;
3774
+ return ((bytes[byteOffset] ?? 0) | (bytes[byteOffset + 1] ?? 0) << 8 | (bytes[byteOffset + 2] ?? 0) << 16 | (bytes[byteOffset + 3] ?? 0) << 24) >>> 0;
3775
+ }
3776
+ return this.readArrayElementAt(bytes, index, arrayMeta.elementType);
3777
+ }
3778
+ /** Read one scalar field without constructing a component shape. */
3779
+ readFieldValue(arch, component, row, fieldName) {
3780
+ return this.table(arch).storage.get(componentId(component))?.fields.get(fieldName)?.view[row];
3781
+ }
3640
3782
  // ──────────────────────────────────────────────────────────────────────────
3641
3783
  // Internal — archetype data read/write
3642
3784
  // ──────────────────────────────────────────────────────────────────────────
@@ -3701,7 +3843,7 @@ var ComponentStorage = class {
3701
3843
  if (!col) return;
3702
3844
  col.view[row] = handle;
3703
3845
  }
3704
- writeRow(arch, component, row, value) {
3846
+ writeRow(arch, component, row, value, options) {
3705
3847
  const localId = componentId(component);
3706
3848
  const fieldCols = this.table(arch).storage.get(localId)?.fields;
3707
3849
  if (!fieldCols) {
@@ -3750,6 +3892,12 @@ var ComponentStorage = class {
3750
3892
  } else {
3751
3893
  const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
3752
3894
  if (arrayMeta !== void 0) {
3895
+ if (options?.skipVariableArrayInitialization === true && arrayMeta.length === void 0) {
3896
+ col.view[row] = 0;
3897
+ const countCol = fieldCols.get(arrayCountColumnName(fieldName));
3898
+ if (countCol !== void 0) countCol.view[row] = 0;
3899
+ continue;
3900
+ }
3753
3901
  this.writeArrayField(arch, component, row, fieldName, fieldType, arrayMeta, raw);
3754
3902
  } else {
3755
3903
  col.view[row] = raw;
@@ -4125,20 +4273,19 @@ var ComponentStorage = class {
4125
4273
  return readArrayElementAt(bytes, idx, elementType);
4126
4274
  }
4127
4275
  /**
4128
- * Materialise a fresh `TypedArray` snapshot for an `array<T,N>` /
4129
- * `array<T>` field at `row` (plan-strategy §2.2 -- read-only snapshot
4130
- * contract; mutations route through `world.set` / `world.push` /
4131
- * `world.pop`).
4276
+ * Materialise a fresh `TypedArray` view for an `array<T,N>` / `array<T>`
4277
+ * field at `row`. This is the internal row-storage path: the returned view
4278
+ * aliases the live column or BufferPool slot. `WorldComponentAccess.get`
4279
+ * detaches relationship target arrays at the public boundary; internal ECS
4280
+ * relationship maintenance and Scene traversal keep this zero-copy path.
4132
4281
  *
4133
4282
  * **Transient view contract (feat-20260602):** for fixed `array<T,N>`
4134
4283
  * columns the returned `TypedArray` aliases the inline column buffer
4135
4284
  * directly (`col.view.subarray(row * arity, ...)`); for variable
4136
4285
  * `array<T>` columns it aliases the live `BufferPool` slot bytes
4137
- * (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both
4138
- * cases the view is valid only until the next structural change. Writing
4139
- * into the snapshot is undefined behaviour -- the contract is read-only
4140
- * and the implementation may switch to a copy in the future without
4141
- * breaking AI users who consume only `length` / index reads.
4286
+ * (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both cases
4287
+ * the view is valid only until the next structural change. Internal callers
4288
+ * must not write through it except via the owner mutation helpers.
4142
4289
  *
4143
4290
  * For variable arrays the typed-array length matches the live count from
4144
4291
  * the sidecar `<fieldName>:count` column; for fixed arrays it matches the
@@ -4397,6 +4544,9 @@ function relationshipPayloadWrites(data) {
4397
4544
  return true;
4398
4545
  });
4399
4546
  }
4547
+ function detachRelationshipTargetArray(value) {
4548
+ return value instanceof Uint32Array ? value.slice() : value;
4549
+ }
4400
4550
  var WorldComponentAccess = class {
4401
4551
  constructor(state) {
4402
4552
  this.state = state;
@@ -4437,16 +4587,19 @@ var WorldComponentAccess = class {
4437
4587
  }
4438
4588
  return index;
4439
4589
  }
4440
- /** Read the World-owned materialized target array; never consults a shadow list. */
4590
+ /**
4591
+ * Read the World-owned materialized target array without a public snapshot.
4592
+ * This is an internal relationship-owner path: it borrows the live
4593
+ * `Uint32Array` so attach/detach stays zero-copy. Public `World.get` detaches
4594
+ * target arrays before returning them to callers.
4595
+ */
4441
4596
  relationshipTargetEntries(source, target) {
4442
4597
  const role = relationshipRole(source);
4443
4598
  if (role?.kind !== "source") return [];
4444
4599
  const mirror = relationshipMirror(source);
4445
4600
  if (mirror === void 0) return [];
4446
- const result = this.get(target, mirror);
4447
- if (!result.ok) return [];
4448
- const entries = result.value[role.targetField];
4449
- return entries !== void 0 && typeof entries === "object" ? entries : [];
4601
+ const entries = this._getArrayView(target, mirror, role.targetField);
4602
+ return entries === void 0 ? [] : entries;
4450
4603
  }
4451
4604
  markComponentAdded(entity, component) {
4452
4605
  this.state.markComponentAdded(entity, componentId(component));
@@ -4526,8 +4679,7 @@ var WorldComponentAccess = class {
4526
4679
  const targetRecord = this.records[entityIndex(target)];
4527
4680
  const actualGeneration = targetRecord?.generation ?? -1;
4528
4681
  const targetLive = this.recordIsLive(targetRecord, entityGeneration(target));
4529
- const holderIsPending = holder === null || pendingEntities?.has(holder) === true;
4530
- if (!targetIsPending && !targetLive && !holderIsPending) {
4682
+ if (!targetIsPending && !targetLive) {
4531
4683
  return err(
4532
4684
  new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
4533
4685
  operation: "relationship-insert",
@@ -4584,7 +4736,14 @@ var WorldComponentAccess = class {
4584
4736
  current = next;
4585
4737
  }
4586
4738
  }
4587
- /** Prepare the target side before a source archetype mutation commits. */
4739
+ /**
4740
+ * Reserve target-side relationship capacity without touching World columns.
4741
+ *
4742
+ * A missing mirror reserves its first slot before the mirror archetype is
4743
+ * created. An existing mirror either reserves an empty slot or grows its
4744
+ * existing BufferPool slot; both failure paths return before any component,
4745
+ * relationship index, or ECS epoch changes.
4746
+ */
4588
4747
  prepareRelationshipInsert(component, value) {
4589
4748
  const role = relationshipRole(component);
4590
4749
  if (role?.kind !== "source") return ok(void 0);
@@ -4605,34 +4764,113 @@ var WorldComponentAccess = class {
4605
4764
  );
4606
4765
  }
4607
4766
  const targetArch = this.graph.archetypes[targetRec.archetypeId];
4608
- const hasMirror = targetArch?.components.some((candidate) => componentId(candidate) === componentId(mirror)) ?? false;
4609
- if (!hasMirror) {
4610
- const added = this._addComponentCore(
4611
- target,
4612
- { component: mirror, data: {} },
4613
- true
4767
+ const mirrorLocalId = componentId(mirror);
4768
+ const mirrorPresent = targetArch?.components.some((candidate) => componentId(candidate) === mirrorLocalId) ?? false;
4769
+ const fieldName = role.targetField;
4770
+ const arrayMeta = componentDefinition(mirror).fields[fieldName]?.arrayMeta;
4771
+ if (arrayMeta === void 0) {
4772
+ return err(new ComponentNotPresentError(target, mirror.name));
4773
+ }
4774
+ const meta = TYPE_METADATA[arrayMeta.elementType];
4775
+ if (meta?.byteSize === void 0) {
4776
+ return err(new ComponentNotPresentError(target, mirror.name));
4777
+ }
4778
+ let currentLength = 0;
4779
+ let slotId = 0;
4780
+ if (mirrorPresent) {
4781
+ currentLength = this._getArrayLength(target, mirror, fieldName) ?? 0;
4782
+ const fieldCols = this.table(targetArch).storage.get(mirrorLocalId)?.fields;
4783
+ const column = fieldCols?.get(fieldName);
4784
+ if (column === void 0) {
4785
+ return err(new ComponentNotPresentError(target, mirror.name));
4786
+ }
4787
+ const row = this.tableRow(targetRec);
4788
+ slotId = column.view[row];
4789
+ }
4790
+ const requiredBytes = (currentLength + 1) * meta.byteSize;
4791
+ if (!Number.isSafeInteger(requiredBytes)) {
4792
+ return err(new ManagedBufferOutOfBoundsError(requiredBytes, Number.MAX_SAFE_INTEGER));
4793
+ }
4794
+ const preparation = {
4795
+ target,
4796
+ mirror,
4797
+ fieldName,
4798
+ mirrorPresent,
4799
+ reservedSlotId: void 0
4800
+ };
4801
+ if (!mirrorPresent || slotId === 0) {
4802
+ const allocated = this.bufferPool.alloc(requiredBytes);
4803
+ if (!allocated.ok) return allocated;
4804
+ preparation.reservedSlotId = allocated.value.id;
4805
+ } else if (this.bufferPool.view(slotId).byteLength < requiredBytes) {
4806
+ const grown = this.bufferPool.grow(slotId, requiredBytes);
4807
+ if (!grown.ok) return grown;
4808
+ }
4809
+ return ok(preparation);
4810
+ }
4811
+ /** Release a target-capacity reservation that did not reach commit. */
4812
+ releaseRelationshipPreparation(preparation) {
4813
+ if (preparation === void 0 || preparation.reservedSlotId === void 0) return;
4814
+ const slotId = preparation.reservedSlotId;
4815
+ this.bufferPool.release(slotId);
4816
+ preparation.reservedSlotId = void 0;
4817
+ }
4818
+ /** Install a reserved slot after the target mirror archetype exists. */
4819
+ installRelationshipPreparation(preparation) {
4820
+ const record = this.lookupAlive(
4821
+ preparation.target,
4822
+ "relationship-capacity",
4823
+ preparation.mirror.name
4824
+ );
4825
+ if (!record.ok) return record;
4826
+ const arch = this.graph.archetypes[record.value.archetypeId];
4827
+ if (arch === void 0) {
4828
+ return err(
4829
+ new ComponentNotPresentError(preparation.target, preparation.mirror.name)
4830
+ );
4831
+ }
4832
+ const fieldCols = this.table(arch).storage.get(componentId(preparation.mirror))?.fields;
4833
+ const column = fieldCols?.get(preparation.fieldName);
4834
+ if (column === void 0) {
4835
+ return err(
4836
+ new ComponentNotPresentError(preparation.target, preparation.mirror.name)
4614
4837
  );
4615
- if (!added.ok) return added;
4616
4838
  }
4617
- const length = this.relationshipTargetEntries(component, target).length;
4618
- return this.ensureArrayCapacity(target, mirror, role.targetField, length + 1);
4839
+ const slotId = preparation.reservedSlotId;
4840
+ if (slotId !== void 0) {
4841
+ column.view[this.tableRow(record.value)] = slotId;
4842
+ preparation.reservedSlotId = void 0;
4843
+ }
4844
+ return ok(void 0);
4619
4845
  }
4620
4846
  /** Append `holder` to the materialized target list. */
4621
- relationshipOnInsert(holder, component, value) {
4847
+ relationshipOnInsert(holder, component, value, preparation) {
4622
4848
  const role = relationshipRole(component);
4623
4849
  if (role?.kind !== "source") return ok(void 0);
4624
4850
  const target = this.relationshipTargetEntity(component, value);
4625
4851
  if (target === null) return ok(void 0);
4626
4852
  const mirror = relationshipMirror(component);
4627
4853
  if (mirror === void 0) return ok(void 0);
4628
- const prepared = this.prepareRelationshipInsert(component, value);
4629
- if (!prepared.ok) {
4630
- if (prepared.error.code === "stale-entity") return ok(void 0);
4631
- return prepared;
4854
+ let prepared = preparation;
4855
+ if (prepared === void 0) {
4856
+ const preparedResult = this.prepareRelationshipInsert(component, value);
4857
+ if (!preparedResult.ok) return preparedResult;
4858
+ prepared = preparedResult.value;
4632
4859
  }
4860
+ if (prepared === void 0) return ok(void 0);
4633
4861
  const targetSlot = entityIndex(target);
4634
4862
  const targetRec = this.records[targetSlot];
4635
- if (!this.recordIsLive(targetRec, entityGeneration(target))) return ok(void 0);
4863
+ const actualGeneration = targetRec?.generation ?? -1;
4864
+ if (!this.recordIsLive(targetRec, entityGeneration(target))) {
4865
+ return err(
4866
+ new StaleEntityError(target, targetSlot, entityGeneration(target), {
4867
+ operation: "relationship-insert",
4868
+ component: component.name,
4869
+ expectedGeneration: entityGeneration(target),
4870
+ actualGeneration
4871
+ })
4872
+ );
4873
+ }
4636
4874
  const targetArch = this.graph.archetypes[targetRec.archetypeId];
4637
4875
  const mirrorLocalId = componentId(mirror);
4638
4876
  const hasMirror = targetArch?.components.some((component2) => componentId(component2) === mirrorLocalId) ?? false;
@@ -4643,9 +4881,19 @@ var WorldComponentAccess = class {
4643
4881
  component: mirror,
4644
4882
  data: {}
4645
4883
  },
4884
+ true,
4885
+ false,
4646
4886
  true
4647
4887
  );
4648
- if (!added.ok) return added;
4888
+ if (!added.ok) {
4889
+ this.releaseRelationshipPreparation(prepared);
4890
+ return added;
4891
+ }
4892
+ }
4893
+ const installed = this.installRelationshipPreparation(prepared);
4894
+ if (!installed.ok) {
4895
+ this.releaseRelationshipPreparation(prepared);
4896
+ return installed;
4649
4897
  }
4650
4898
  const targetEntries = this.relationshipTargetEntries(component, target);
4651
4899
  const slot = targetEntries.length;
@@ -4655,7 +4903,10 @@ var WorldComponentAccess = class {
4655
4903
  role.targetField,
4656
4904
  holder
4657
4905
  );
4658
- if (!mirrored.ok) return mirrored;
4906
+ if (!mirrored.ok) {
4907
+ this.releaseRelationshipPreparation(prepared);
4908
+ return mirrored;
4909
+ }
4659
4910
  this.relationshipIndex(component)?.attach(holder, target, slot);
4660
4911
  return ok(void 0);
4661
4912
  }
@@ -4688,15 +4939,16 @@ var WorldComponentAccess = class {
4688
4939
  /**
4689
4940
  * Read component data from an entity.
4690
4941
  *
4691
- * **Transient view contract (feat-20260602):** for fixed-capacity
4692
- * `array<T,N>` and `buffer<N>` fields, the returned `TypedArray` (and any
4693
- * subarray of it) aliases the archetype column buffer directly. The view is
4694
- * valid only until the next structural change (`spawn` / `despawn` /
4695
- * `addComponent` / `removeComponent`). Holding a view across a structural
4696
- * change is undefined behaviour -- the backing `ArrayBuffer` is detached on
4697
- * column growth, and swap-remove at the same row index points to the wrong
4698
- * entity. **Re-fetch `world.get(e, C)` on every access.** See
4699
- * `packages/ecs/README.md` Transient view contract section.
4942
+ * **Public array contract:** relationship target `array<entity>` fields are
4943
+ * detached `Uint32Array` copies. Mutating that returned array cannot alter
4944
+ * the materialized target, relationship index, or source. Other array
4945
+ * fields retain the existing transient view contract: fixed-capacity
4946
+ * `array<T,N>` and `buffer<N>` fields alias the archetype column buffer
4947
+ * directly, while variable managed arrays alias their BufferPool slot. Those
4948
+ * views are valid only until the next structural change (`spawn` /
4949
+ * `despawn` / `addComponent` / `removeComponent`); callers must re-fetch
4950
+ * `world.get(e, C)` on every access. Internal owners use `readRow` and
4951
+ * `_getArrayView` directly and retain zero-copy access.
4700
4952
  *
4701
4953
  * @returns `Result<ShapeOf<S>, EcsError>` —
4702
4954
  * `ok(ShapeOf<S>)` on success;
@@ -4736,7 +4988,13 @@ var WorldComponentAccess = class {
4736
4988
  if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
4737
4989
  return err(new ComponentNotPresentError(entity, component.name));
4738
4990
  }
4739
- return ok(this.storage.readRow(arch, component, this.tableRow(rec)));
4991
+ const value = this.storage.readRow(arch, component, this.tableRow(rec));
4992
+ const role = relationshipRole(component);
4993
+ if (role?.kind === "target") {
4994
+ const targetValue = value;
4995
+ targetValue[role.targetField] = detachRelationshipTargetArray(targetValue[role.targetField]);
4996
+ }
4997
+ return ok(value);
4740
4998
  }
4741
4999
  /**
4742
5000
  * Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
@@ -4770,9 +5028,10 @@ var WorldComponentAccess = class {
4770
5028
  * Returns `undefined` when the entity is dead, the component is absent, the
4771
5029
  * field does not exist, or the field is not an `array<...>` column.
4772
5030
  *
4773
- * @internal Engine-internal fast path; AI users read the typed view through
4774
- * `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
4775
- * route the propagate kernel and render walk use.
5031
+ * @internal Engine-internal fast path; AI users read public component values
5032
+ * through `world.get`. This accessor is the zero-materialization route the
5033
+ * propagate kernel, relationship owner, and render walk use; it bypasses
5034
+ * the detached public relationship-target snapshot.
4776
5035
  */
4777
5036
  _getArrayView(entity, component, fieldName) {
4778
5037
  const record = this.lookupAlive(entity, "_getArrayView", component.name);
@@ -4782,6 +5041,109 @@ var WorldComponentAccess = class {
4782
5041
  if (!arch) return void 0;
4783
5042
  return this.storage.readArrayView(arch, component, this.tableRow(rec), fieldName);
4784
5043
  }
5044
+ /** Internal zero-materialisation read for ECS-owned relationship lists. */
5045
+ _getArrayLength(entity, component, fieldName) {
5046
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5047
+ if (!record.ok) return void 0;
5048
+ const arch = this.graph.archetypes[record.value.archetypeId];
5049
+ if (arch === void 0) return void 0;
5050
+ return this.storage.readArrayLength(arch, component, this.tableRow(record.value), fieldName);
5051
+ }
5052
+ /** Internal zero-materialisation read for one ECS-owned array element. */
5053
+ _getArrayElement(entity, component, fieldName, index) {
5054
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5055
+ if (!record.ok) return void 0;
5056
+ const arch = this.graph.archetypes[record.value.archetypeId];
5057
+ if (arch === void 0) return void 0;
5058
+ return this.storage.readArrayElement(
5059
+ arch,
5060
+ component,
5061
+ this.tableRow(record.value),
5062
+ fieldName,
5063
+ index
5064
+ );
5065
+ }
5066
+ /** Internal scalar-column read used by parent-first hierarchy traversal. */
5067
+ _getFieldValue(entity, component, fieldName) {
5068
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5069
+ if (!record.ok) return void 0;
5070
+ const arch = this.graph.archetypes[record.value.archetypeId];
5071
+ if (arch === void 0) return void 0;
5072
+ return this.storage.readFieldValue(arch, component, this.tableRow(record.value), fieldName);
5073
+ }
5074
+ /**
5075
+ * Converge one writable relationship source mutation through the source
5076
+ * owner. The source scalar and its materialized target list are committed as
5077
+ * one operation; no caller receives a raw source column view that could
5078
+ * bypass the mirror/index maintenance.
5079
+ */
5080
+ setRelationshipSource(entity, component, value, record, arch, markChanged) {
5081
+ const role = relationshipRole(component);
5082
+ if (role?.kind !== "source") return ok(void 0);
5083
+ const row = this.tableRow(record);
5084
+ const current = this.storage.readRow(arch, component, row);
5085
+ const valuePreflight = this.preflightComponentFieldValues(entity, {
5086
+ component,
5087
+ data: value
5088
+ });
5089
+ if (!valuePreflight.ok) return valuePreflight;
5090
+ const merged = { ...current, ...value };
5091
+ const enumError = validateEnumFieldValues(component, merged, entity);
5092
+ if (enumError !== null) return err(enumError);
5093
+ const oldTarget = this.relationshipTargetEntity(component, current);
5094
+ const target = this.relationshipTargetEntity(component, merged);
5095
+ if (target !== null) {
5096
+ const targetRecord = this.records[entityIndex(target)];
5097
+ const actualGeneration = targetRecord?.generation ?? -1;
5098
+ if (!this.recordIsLive(targetRecord, entityGeneration(target))) {
5099
+ return err(
5100
+ new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
5101
+ operation: "relationship-insert",
5102
+ component: component.name,
5103
+ expectedGeneration: entityGeneration(target),
5104
+ actualGeneration
5105
+ })
5106
+ );
5107
+ }
5108
+ const roleAllowsSelf = role.allowSelf;
5109
+ if (entity === target && !roleAllowsSelf) {
5110
+ return err(
5111
+ new RelationshipSelfCycleError(component.name, entity, target)
5112
+ );
5113
+ }
5114
+ const cycleHit = entity === target && roleAllowsSelf ? null : this.relationshipCycleHit(component, target, entity);
5115
+ if (cycleHit !== null) {
5116
+ return err(
5117
+ new RelationshipSelfCycleError(component.name, entity, cycleHit)
5118
+ );
5119
+ }
5120
+ }
5121
+ if (oldTarget !== target) {
5122
+ const prepared = this.prepareRelationshipInsert(component, merged);
5123
+ if (!prepared.ok) return prepared;
5124
+ const preparation = prepared.value;
5125
+ if (oldTarget !== null) {
5126
+ const detached = this.relationshipOnRemove(entity, component, current);
5127
+ if (!detached.ok) {
5128
+ this.releaseRelationshipPreparation(preparation);
5129
+ return detached;
5130
+ }
5131
+ }
5132
+ const attached = this.relationshipOnInsert(entity, component, merged, preparation);
5133
+ if (!attached.ok) {
5134
+ this.releaseRelationshipPreparation(preparation);
5135
+ return attached;
5136
+ }
5137
+ }
5138
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
5139
+ const sourceColumn = fieldCols?.get(role.sourceField);
5140
+ if (sourceColumn === void 0) {
5141
+ return err(new ComponentNotPresentError(entity, component.name));
5142
+ }
5143
+ sourceColumn.view[row] = target === null ? ENTITY_NULL_RAW : target;
5144
+ if (markChanged) this.markComponentChanged(entity, component);
5145
+ return ok(void 0);
5146
+ }
4785
5147
  /**
4786
5148
  * Write (partial) component data to an entity.
4787
5149
  *
@@ -4820,6 +5182,20 @@ var WorldComponentAccess = class {
4820
5182
  if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
4821
5183
  return err(new ComponentNotPresentError(entity, component.name));
4822
5184
  }
5185
+ const role = relationshipRole(component);
5186
+ if (role?.kind === "target") {
5187
+ return err(new RelationshipTargetReadonlyError(component.name, "set"));
5188
+ }
5189
+ if (role?.kind === "source") {
5190
+ return this.setRelationshipSource(
5191
+ entity,
5192
+ component,
5193
+ value,
5194
+ rec,
5195
+ arch,
5196
+ markChanged
5197
+ );
5198
+ }
4823
5199
  const valuePreflight = this.preflightComponentFieldValues(entity, {
4824
5200
  component,
4825
5201
  data: value
@@ -4986,51 +5362,6 @@ var WorldComponentAccess = class {
4986
5362
  this.markComponentChanged(entity, component);
4987
5363
  return ok(void 0);
4988
5364
  }
4989
- ensureArrayCapacity(entity, component, fieldName, minimum) {
4990
- const record = this.lookupAlive(entity, "relationship-capacity", component.name);
4991
- if (!record.ok) return record;
4992
- const rec = record.value;
4993
- const arch = this.graph.archetypes[rec.archetypeId];
4994
- if (!arch) {
4995
- return err(
4996
- new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
4997
- operation: "relationship-capacity",
4998
- component: component.name,
4999
- expectedGeneration: entityGeneration(entity),
5000
- actualGeneration: rec.generation
5001
- })
5002
- );
5003
- }
5004
- const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
5005
- if (!fieldCols) return err(new ComponentNotPresentError(entity, component.name));
5006
- const fieldNameStr = fieldName;
5007
- const col = fieldCols.get(fieldNameStr);
5008
- if (!col) return err(new ComponentNotPresentError(entity, component.name));
5009
- const arrayMeta = componentDefinition(component).fields[fieldNameStr]?.arrayMeta;
5010
- if (arrayMeta === void 0) {
5011
- return err(new ComponentNotPresentError(entity, component.name));
5012
- }
5013
- const meta = TYPE_METADATA[arrayMeta.elementType];
5014
- if (!meta?.byteSize) {
5015
- return err(new ComponentNotPresentError(entity, component.name));
5016
- }
5017
- const maximum = Math.floor(262144 / meta.byteSize);
5018
- if (!Number.isSafeInteger(minimum) || minimum < 0 || minimum > maximum) {
5019
- return err(new ManagedBufferOutOfBoundsError(minimum, maximum));
5020
- }
5021
- const byteLength = minimum * meta.byteSize;
5022
- const slotId = col.view[this.tableRow(rec)];
5023
- if (slotId === 0) {
5024
- if (minimum === 0) return ok(void 0);
5025
- const allocated = this.bufferPool.alloc(byteLength);
5026
- if (!allocated.ok) return allocated;
5027
- col.view[this.tableRow(rec)] = allocated.value.id;
5028
- return ok(void 0);
5029
- }
5030
- if (this.bufferPool.view(slotId).byteLength >= byteLength) return ok(void 0);
5031
- const grown = this.bufferPool.grow(slotId, byteLength);
5032
- return grown.ok ? ok(void 0) : grown;
5033
- }
5034
5365
  /**
5035
5366
  * Remove one variable-array element at a known slot. Relationship holders
5036
5367
  * supply the slot from their backpointer, so this is O(1) and never scans
@@ -5098,7 +5429,7 @@ var WorldComponentAccess = class {
5098
5429
  * (lazy mirror create or exclusive reparent).
5099
5430
  * @internal
5100
5431
  */
5101
- _addComponentCore(entity, componentData, internal, resolveRequirements = true) {
5432
+ _addComponentCore(entity, componentData, internal, resolveRequirements = true, skipVariableArrayInitialization = false) {
5102
5433
  const record = this.lookupAlive(entity, "addComponent", componentData.component.name);
5103
5434
  if (!record.ok) return record;
5104
5435
  const rec = record.value;
@@ -5144,6 +5475,19 @@ var WorldComponentAccess = class {
5144
5475
  if (enumErr !== null) {
5145
5476
  return err(enumErr);
5146
5477
  }
5478
+ const localId = componentId(componentData.component);
5479
+ let relationshipPreparation;
5480
+ const componentAlreadyPresent = srcArch.components.some(
5481
+ (candidate) => componentId(candidate) === localId
5482
+ );
5483
+ if (!internal && !componentAlreadyPresent && relationshipRole(componentData.component)?.kind === "source") {
5484
+ const prepared = this.prepareRelationshipInsert(
5485
+ componentData.component,
5486
+ filled
5487
+ );
5488
+ if (!prepared.ok) return prepared;
5489
+ relationshipPreparation = prepared.value;
5490
+ }
5147
5491
  if (resolveRequirements) {
5148
5492
  const required = expandComponentRequirements([componentData]).slice(1);
5149
5493
  for (const requirement of required) {
@@ -5153,9 +5497,13 @@ var WorldComponentAccess = class {
5153
5497
  continue;
5154
5498
  }
5155
5499
  const added = this._addComponentCore(entity, requirement, internal, false);
5156
- if (!added.ok) return added;
5500
+ if (!added.ok) {
5501
+ this.releaseRelationshipPreparation(relationshipPreparation);
5502
+ return added;
5503
+ }
5157
5504
  srcArch = this.graph.archetypes[rec.archetypeId];
5158
5505
  if (!srcArch) {
5506
+ this.releaseRelationshipPreparation(relationshipPreparation);
5159
5507
  return err(
5160
5508
  new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
5161
5509
  operation: "addComponent",
@@ -5167,32 +5515,21 @@ var WorldComponentAccess = class {
5167
5515
  }
5168
5516
  }
5169
5517
  }
5170
- const localId = componentId(componentData.component);
5171
5518
  if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
5519
+ this.releaseRelationshipPreparation(relationshipPreparation);
5172
5520
  const role = relationshipRole(componentData.component);
5173
5521
  if (role?.kind === "source" && role.exclusive && !internal) {
5174
- const prepared = this.prepareRelationshipInsert(
5175
- componentData.component,
5176
- filled
5177
- );
5178
- if (!prepared.ok) return prepared;
5179
- const removeR = this._removeComponentCore(
5522
+ return this.setRelationshipSource(
5180
5523
  entity,
5181
5524
  componentData.component,
5182
- false
5525
+ filled,
5526
+ rec,
5527
+ srcArch,
5528
+ true
5183
5529
  );
5184
- if (!removeR.ok) return removeR;
5185
- return this._addComponentCore(entity, componentData, false);
5186
5530
  }
5187
5531
  return err(new ComponentAlreadyPresentError(entity, componentData.component.name));
5188
5532
  }
5189
- if (!internal && relationshipRole(componentData.component)?.kind === "source") {
5190
- const prepared = this.prepareRelationshipInsert(
5191
- componentData.component,
5192
- filled
5193
- );
5194
- if (!prepared.ok) return prepared;
5195
- }
5196
5533
  const targetArch = getAddEdge(
5197
5534
  this.graph,
5198
5535
  srcArch,
@@ -5209,7 +5546,8 @@ var WorldComponentAccess = class {
5209
5546
  targetArch,
5210
5547
  componentData.component,
5211
5548
  this.tableRow(rec),
5212
- filled
5549
+ filled,
5550
+ skipVariableArrayInitialization ? { skipVariableArrayInitialization: true } : void 0
5213
5551
  );
5214
5552
  }
5215
5553
  this.markComponentAdded(entity, componentData.component);
@@ -5217,9 +5555,13 @@ var WorldComponentAccess = class {
5217
5555
  const relationshipResult = this.relationshipOnInsert(
5218
5556
  entity,
5219
5557
  componentData.component,
5220
- filled
5558
+ filled,
5559
+ relationshipPreparation
5221
5560
  );
5222
- if (!relationshipResult.ok) return relationshipResult;
5561
+ if (!relationshipResult.ok) {
5562
+ this.releaseRelationshipPreparation(relationshipPreparation);
5563
+ return relationshipResult;
5564
+ }
5223
5565
  }
5224
5566
  this.markStructureChanged();
5225
5567
  this.state.recordStructuralEvidence({
@@ -5347,6 +5689,35 @@ var WorldComponentAccess = class {
5347
5689
  const slot = entityIndex(entity);
5348
5690
  const record = this.records[slot];
5349
5691
  if (!record || record.archetypeId !== -1) return ok(void 0);
5692
+ const relationshipPreparations = [];
5693
+ for (let index = 0; index < componentDatas.length; index += 1) {
5694
+ const componentData = componentDatas[index];
5695
+ if (componentData === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
5696
+ continue;
5697
+ }
5698
+ const filled = fillComponentDefaults(
5699
+ componentData.component,
5700
+ componentData.data
5701
+ );
5702
+ const target = this.relationshipTargetEntity(
5703
+ componentData.component,
5704
+ filled
5705
+ );
5706
+ const targetRecord = target === null ? void 0 : this.records[entityIndex(target)];
5707
+ if (target !== null && targetRecord !== void 0 && this.recordIsLive(targetRecord, entityGeneration(target))) {
5708
+ const prepared = this.prepareRelationshipInsert(
5709
+ componentData.component,
5710
+ filled
5711
+ );
5712
+ if (!prepared.ok) {
5713
+ for (const reservation of relationshipPreparations) {
5714
+ this.releaseRelationshipPreparation(reservation);
5715
+ }
5716
+ return prepared;
5717
+ }
5718
+ relationshipPreparations[index] = prepared.value;
5719
+ }
5720
+ }
5350
5721
  const componentIds = componentDatas.map((cd) => componentId(cd.component));
5351
5722
  const components = componentDatas.map((cd) => cd.component);
5352
5723
  const arch = getOrCreateArchetype(this.graph, componentIds, components);
@@ -5364,15 +5735,21 @@ var WorldComponentAccess = class {
5364
5735
  this.storage.writeRow(arch, cd.component, tableRow2, filled);
5365
5736
  }
5366
5737
  this.storage.writeEntitySelf(arch, tableRow2, entity);
5367
- for (const cd of componentDatas) {
5738
+ for (let index = 0; index < componentDatas.length; index += 1) {
5739
+ const cd = componentDatas[index];
5740
+ if (cd === void 0) continue;
5368
5741
  if (relationshipRole(cd.component)?.kind === "source") {
5369
5742
  const filled = fillComponentDefaults(cd.component, cd.data);
5370
5743
  const relationshipResult = this.relationshipOnInsert(
5371
5744
  entity,
5372
5745
  cd.component,
5373
- filled
5746
+ filled,
5747
+ relationshipPreparations[index]
5374
5748
  );
5375
5749
  if (!relationshipResult.ok) {
5750
+ for (const reservation of relationshipPreparations) {
5751
+ this.releaseRelationshipPreparation(reservation);
5752
+ }
5376
5753
  return relationshipResult;
5377
5754
  }
5378
5755
  }
@@ -5472,10 +5849,10 @@ var BufferPool = class {
5472
5849
  * Allocate a managed buffer slot of at least `byteLength` bytes.
5473
5850
  *
5474
5851
  * Routes:
5475
- * - byteLength < 0 -> not in current contract (caller responsibility).
5852
+ * - invalid byteLength -> structured out-of-bounds error.
5476
5853
  * - byteLength == 0 -> ok({ id, view: zero-length Uint8Array }) (no bucket).
5477
5854
  * - byteLength <= 262144 -> ok({ id, view }), bucket = smallest >= byteLength.
5478
- * - byteLength > 262144 -> err(managed-buffer-out-of-bounds).
5855
+ * - byteLength > 262144 -> a dedicated allocation; allocation failure is structured.
5479
5856
  *
5480
5857
  * D-5: size classes are radix-4 (16 / 64 / 256 / 1K / 4K / 16K / 64K / 256K).
5481
5858
  * Free-list pop reuses the most recently released slot id at the same bucket;
@@ -5489,11 +5866,20 @@ var BufferPool = class {
5489
5866
  this.slots.set(id2, { sizeClassIdx: -1, buffer: buffer2, view: view2, byteLength: 0, live: true });
5490
5867
  return ok({ id: id2, view: view2 });
5491
5868
  }
5869
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
5870
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
5871
+ }
5492
5872
  const idx = bucketIndex(byteLength);
5493
5873
  if (idx === SIZE_CLASSES.length) {
5494
- return err(
5495
- new ManagedBufferOutOfBoundsError(byteLength, SIZE_CLASSES[SIZE_CLASSES.length - 1] ?? 0)
5496
- );
5874
+ try {
5875
+ const buffer2 = new ArrayBuffer(byteLength);
5876
+ const view2 = new Uint8Array(buffer2);
5877
+ const id2 = this.nextId++;
5878
+ this.slots.set(id2, { sizeClassIdx: idx, buffer: buffer2, view: view2, byteLength, live: true });
5879
+ return ok({ id: id2, view: view2 });
5880
+ } catch {
5881
+ return err(new ManagedBufferOutOfBoundsError(byteLength, 0));
5882
+ }
5497
5883
  }
5498
5884
  const bucketBytes = SIZE_CLASSES[idx];
5499
5885
  if (bucketBytes === void 0) {
@@ -5537,7 +5923,7 @@ var BufferPool = class {
5537
5923
  * the caller become detached / orphaned - callers must use `pool.view(id)`
5538
5924
  * after grow to read the refreshed view (the `release` loop refreshes
5539
5925
  * automatically).
5540
- * - newBytes > 262144 -> err(managed-buffer-out-of-bounds).
5926
+ * - newBytes beyond the last pooled class -> dedicated allocation.
5541
5927
  */
5542
5928
  grow(id, newBytes) {
5543
5929
  const slot = this.slots.get(id);
@@ -5550,25 +5936,27 @@ var BufferPool = class {
5550
5936
  if (newBytes === slot.byteLength) {
5551
5937
  return ok(slot.view);
5552
5938
  }
5553
- const newIdx = bucketIndex(newBytes);
5554
- if (newIdx === SIZE_CLASSES.length) {
5555
- return err(
5556
- new ManagedBufferOutOfBoundsError(newBytes, SIZE_CLASSES[SIZE_CLASSES.length - 1] ?? 0)
5557
- );
5939
+ if (!Number.isSafeInteger(newBytes) || newBytes < 0) {
5940
+ return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
5558
5941
  }
5559
- if (newIdx === slot.sizeClassIdx) {
5942
+ const newIdx = bucketIndex(newBytes);
5943
+ if (newBytes <= slot.buffer.byteLength) {
5560
5944
  slot.byteLength = newBytes;
5561
5945
  slot.view = new Uint8Array(slot.buffer, 0, newBytes);
5562
5946
  return ok(slot.view);
5563
5947
  }
5564
- const newBucketBytes = SIZE_CLASSES[newIdx];
5948
+ const newBucketBytes = SIZE_CLASSES[newIdx] ?? Math.max(newBytes, slot.buffer.byteLength * 2);
5565
5949
  const oldByteLength = slot.byteLength;
5566
5950
  let nextBuffer;
5567
- if (HAS_TRANSFER2) {
5568
- nextBuffer = slot.buffer.transfer(newBucketBytes);
5569
- } else {
5570
- nextBuffer = new ArrayBuffer(newBucketBytes);
5571
- new Uint8Array(nextBuffer).set(new Uint8Array(slot.buffer, 0, oldByteLength));
5951
+ try {
5952
+ if (HAS_TRANSFER2) {
5953
+ nextBuffer = slot.buffer.transfer(newBucketBytes);
5954
+ } else {
5955
+ nextBuffer = new ArrayBuffer(newBucketBytes);
5956
+ new Uint8Array(nextBuffer).set(new Uint8Array(slot.buffer, 0, oldByteLength));
5957
+ }
5958
+ } catch {
5959
+ return err(new ManagedBufferOutOfBoundsError(newBytes, slot.buffer.byteLength));
5572
5960
  }
5573
5961
  slot.sizeClassIdx = newIdx;
5574
5962
  slot.buffer = nextBuffer;
@@ -5587,7 +5975,7 @@ var BufferPool = class {
5587
5975
  if (slot === void 0) return ok(void 0);
5588
5976
  if (!slot.live) return ok(void 0);
5589
5977
  slot.live = false;
5590
- if (slot.sizeClassIdx >= 0) {
5978
+ if (slot.sizeClassIdx >= 0 && slot.sizeClassIdx < SIZE_CLASSES.length) {
5591
5979
  const bucket = this.freeBuckets[slot.sizeClassIdx];
5592
5980
  if (bucket !== void 0) bucket.push(id);
5593
5981
  } else {
@@ -5616,7 +6004,7 @@ var BufferPool = class {
5616
6004
  const slot = this.slots.get(id);
5617
6005
  if (slot === void 0 || !slot.live) return 0;
5618
6006
  if (slot.sizeClassIdx < 0) return 0;
5619
- return SIZE_CLASSES[slot.sizeClassIdx] ?? 0;
6007
+ return slot.buffer.byteLength;
5620
6008
  }
5621
6009
  /**
5622
6010
  * Reset the logical byteLength of slot `id` to `newByteLength` while
@@ -5644,7 +6032,7 @@ var BufferPool = class {
5644
6032
  }
5645
6033
  return err(new ManagedBufferOutOfBoundsError(newByteLength, 0));
5646
6034
  }
5647
- const bucketBytes = SIZE_CLASSES[slot.sizeClassIdx] ?? 0;
6035
+ const bucketBytes = slot.buffer.byteLength;
5648
6036
  if (newByteLength > bucketBytes) {
5649
6037
  return err(new ManagedBufferOutOfBoundsError(newByteLength, bucketBytes));
5650
6038
  }
@@ -6137,6 +6525,27 @@ function spawnCore(world, componentDatas, internal) {
6137
6525
  if (enumErr !== null) return err(enumErr);
6138
6526
  filledData.push(filled);
6139
6527
  }
6528
+ const relationshipPreparations = [];
6529
+ if (!internal) {
6530
+ for (let index = 0; index < componentDatas.length; index += 1) {
6531
+ const componentData = componentDatas[index];
6532
+ const value = filledData[index];
6533
+ if (componentData === void 0 || value === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
6534
+ continue;
6535
+ }
6536
+ const prepared = world[worldInternal].prepareRelationshipInsert(
6537
+ componentData.component,
6538
+ value
6539
+ );
6540
+ if (!prepared.ok) {
6541
+ for (const reservation of relationshipPreparations) {
6542
+ world[worldInternal].releaseRelationshipPreparation(reservation);
6543
+ }
6544
+ return prepared;
6545
+ }
6546
+ relationshipPreparations[index] = prepared.value;
6547
+ }
6548
+ }
6140
6549
  const indexSlot = world[worldInternal].allocateIndex();
6141
6550
  const record = world[worldInternal].getRecords()[indexSlot];
6142
6551
  if (record === void 0)
@@ -6172,9 +6581,15 @@ function spawnCore(world, componentDatas, internal) {
6172
6581
  const relation = world[worldInternal].relationshipOnInsert(
6173
6582
  spawnedEntity,
6174
6583
  cd.component,
6175
- filled
6584
+ filled,
6585
+ relationshipPreparations[i]
6176
6586
  );
6177
- if (!relation.ok) return relation;
6587
+ if (!relation.ok) {
6588
+ for (const reservation of relationshipPreparations) {
6589
+ world[worldInternal].releaseRelationshipPreparation(reservation);
6590
+ }
6591
+ return relation;
6592
+ }
6178
6593
  }
6179
6594
  }
6180
6595
  world[worldInternal].markStructureChanged();
@@ -6248,7 +6663,8 @@ function despawnCore(world, entity, internal) {
6248
6663
  }
6249
6664
  function worldAddChild(world, parent, child, component, data) {
6250
6665
  const holderComp = component;
6251
- if (relationshipRole(holderComp)?.kind !== "source") {
6666
+ const role = relationshipRole(holderComp);
6667
+ if (role?.kind !== "source") {
6252
6668
  return err(new ComponentNotPresentError(child, component.name));
6253
6669
  }
6254
6670
  const parentSlot = entityIndex(parent);
@@ -6277,7 +6693,6 @@ function worldAddChild(world, parent, child, component, data) {
6277
6693
  })
6278
6694
  );
6279
6695
  }
6280
- const role = relationshipRole(holderComp);
6281
6696
  if (child === parent && !(role?.kind === "source" && role.allowSelf)) {
6282
6697
  return err(new RelationshipSelfCycleError(component.name, child, child));
6283
6698
  }
@@ -6328,10 +6743,10 @@ function worldRemoveChild(world, parent, child, component) {
6328
6743
  }
6329
6744
  function worldReparent(world, child, newParent, component, data) {
6330
6745
  const holderComp = component;
6331
- if (relationshipRole(holderComp)?.kind !== "source") {
6746
+ const role = relationshipRole(holderComp);
6747
+ if (role?.kind !== "source") {
6332
6748
  return err(new ComponentNotPresentError(child, component.name));
6333
6749
  }
6334
- const role = relationshipRole(holderComp);
6335
6750
  if (child === newParent && !(role?.kind === "source" && role.allowSelf)) {
6336
6751
  return err(
6337
6752
  new RelationshipSelfCycleError(component.name, child, newParent)
@@ -6361,11 +6776,14 @@ function worldReparent(world, child, newParent, component, data) {
6361
6776
  })
6362
6777
  );
6363
6778
  }
6364
- if (childArch.components.some((component2) => componentId(component2) === componentId(holderComp))) {
6365
- const removeResult = world.removeComponent(child, component);
6366
- if (!removeResult.ok) return removeResult;
6779
+ const payload = {
6780
+ ...data,
6781
+ [role.sourceField]: newParent
6782
+ };
6783
+ if (childArch.components.some((candidate) => componentId(candidate) === componentId(holderComp))) {
6784
+ return world.set(child, component, payload);
6367
6785
  }
6368
- return world.addComponent(child, { component, data });
6786
+ return world.addComponent(child, { component, data: payload });
6369
6787
  }
6370
6788
  function worldIterAncestors(world, entity) {
6371
6789
  return {
@@ -6986,6 +7404,9 @@ var World = class {
6986
7404
  cancelPendingEntity: this.internalcancelPendingEntity.bind(this),
6987
7405
  despawnCore: this.internaldespawnCore.bind(this),
6988
7406
  getArrayView: this.internalgetArrayView.bind(this),
7407
+ getArrayLength: this.internalgetArrayLength.bind(this),
7408
+ getArrayElement: this.internalgetArrayElement.bind(this),
7409
+ getFieldValue: this.internalgetFieldValue.bind(this),
6989
7410
  getBufferPool: this.internalgetBufferPool.bind(this),
6990
7411
  getClockWriter: this.internalgetClockWriter.bind(this),
6991
7412
  getComponentChange: this.internalgetComponentChange.bind(this),
@@ -7016,6 +7437,7 @@ var World = class {
7016
7437
  materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
7017
7438
  nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
7018
7439
  poisonExecution: this.internalpoisonExecution.bind(this),
7440
+ prepareRelationshipInsert: this.internalprepareRelationshipInsert.bind(this),
7019
7441
  publishDerivedRange: this.internalpublishDerivedRange.bind(this),
7020
7442
  preflightComponentData: this.internalpreflightComponentData.bind(this),
7021
7443
  readRow: this.internalreadRow.bind(this),
@@ -7024,6 +7446,7 @@ var World = class {
7024
7446
  relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
7025
7447
  relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
7026
7448
  releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
7449
+ releaseRelationshipPreparation: this.internalreleaseRelationshipPreparation.bind(this),
7027
7450
  removeComponentCore: this.internalremoveComponentCore.bind(this),
7028
7451
  routeError: this.internalrouteError.bind(this),
7029
7452
  restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
@@ -7175,7 +7598,12 @@ var World = class {
7175
7598
  }
7176
7599
  /** Query facade write after the facade has already marked evidence. */
7177
7600
  internalsetQueryRow(entity, component, value) {
7178
- return this.componentAccess.set(entity, component, value, false);
7601
+ return this.componentAccess.set(
7602
+ entity,
7603
+ component,
7604
+ value,
7605
+ relationshipRole(component)?.kind === "source"
7606
+ );
7179
7607
  }
7180
7608
  /** Query facade read that does not re-enter the public World API. */
7181
7609
  internalgetQueryRow(entity, component) {
@@ -7523,6 +7951,15 @@ var World = class {
7523
7951
  internalgetArrayView(entity, component, fieldName) {
7524
7952
  return this.componentAccess._getArrayView(entity, component, fieldName);
7525
7953
  }
7954
+ internalgetArrayLength(entity, component, fieldName) {
7955
+ return this.componentAccess._getArrayLength(entity, component, fieldName);
7956
+ }
7957
+ internalgetArrayElement(entity, component, fieldName, index) {
7958
+ return this.componentAccess._getArrayElement(entity, component, fieldName, index);
7959
+ }
7960
+ internalgetFieldValue(entity, component, fieldName) {
7961
+ return this.componentAccess._getFieldValue(entity, component, fieldName);
7962
+ }
7526
7963
  set(entity, component, value) {
7527
7964
  if (isRelationshipTarget(component)) return this.relationshipTargetWriteError(component, "set");
7528
7965
  return this.componentAccess.set(entity, component, value);
@@ -7591,8 +8028,16 @@ var World = class {
7591
8028
  this.componentAccess.releaseManagedRefsOnRow(a, c, r);
7592
8029
  }
7593
8030
  /** */
7594
- internalrelationshipOnInsert(h, c, v) {
7595
- return this.componentAccess.relationshipOnInsert(h, c, v);
8031
+ internalrelationshipOnInsert(h, c, v, preparation) {
8032
+ return this.componentAccess.relationshipOnInsert(h, c, v, preparation);
8033
+ }
8034
+ /** */
8035
+ internalprepareRelationshipInsert(c, v) {
8036
+ return this.componentAccess.prepareRelationshipInsert(c, v);
8037
+ }
8038
+ /** */
8039
+ internalreleaseRelationshipPreparation(preparation) {
8040
+ this.componentAccess.releaseRelationshipPreparation(preparation);
7596
8041
  }
7597
8042
  /** */
7598
8043
  internalrelationshipOnRemove(h, c, v) {