@forgeax/engine-ecs 0.1.23 → 0.1.24

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 (38) hide show
  1. package/README.md +33 -1
  2. package/dist/component.d.ts +9 -6
  3. package/dist/component.d.ts.map +1 -1
  4. package/dist/errors/query-and-component-errors.d.ts +1 -1
  5. package/dist/errors/query-and-component-errors.d.ts.map +1 -1
  6. package/dist/externalization/index.mjs.map +1 -1
  7. package/dist/index.mjs +566 -131
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/internal.d.ts +3 -1
  10. package/dist/internal.d.ts.map +1 -1
  11. package/dist/internal.mjs +6 -1
  12. package/dist/internal.mjs.map +1 -1
  13. package/dist/projection/index.mjs.map +1 -1
  14. package/dist/query/derived-range-writer.d.ts +14 -0
  15. package/dist/query/derived-range-writer.d.ts.map +1 -1
  16. package/dist/query/query.d.ts.map +1 -1
  17. package/dist/shared.mjs.map +1 -1
  18. package/dist/world-component-access.d.ts +63 -18
  19. package/dist/world-component-access.d.ts.map +1 -1
  20. package/dist/world-component-storage.d.ts +26 -10
  21. package/dist/world-component-storage.d.ts.map +1 -1
  22. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  23. package/dist/world-internal.d.ts +1 -1
  24. package/dist/world-internal.d.ts.map +1 -1
  25. package/dist/world.d.ts +5 -0
  26. package/dist/world.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/__tests__/relationship-index.test.ts +147 -1
  29. package/src/component.ts +9 -6
  30. package/src/errors/query-and-component-errors.ts +4 -1
  31. package/src/internal.ts +3 -0
  32. package/src/query/derived-range-writer.ts +111 -0
  33. package/src/query/query.ts +26 -1
  34. package/src/world-component-access.ts +428 -127
  35. package/src/world-component-storage.ts +104 -18
  36. package/src/world-entity-lifecycle.ts +49 -9
  37. package/src/world-internal.ts +5 -0
  38. 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,114 @@ 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
+ const maximum = Math.floor(262144 / meta.byteSize);
4792
+ if (!Number.isSafeInteger(currentLength + 1) || currentLength + 1 > maximum) {
4793
+ return err(new ManagedBufferOutOfBoundsError(requiredBytes, 262144));
4794
+ }
4795
+ const preparation = {
4796
+ target,
4797
+ mirror,
4798
+ fieldName,
4799
+ mirrorPresent,
4800
+ reservedSlotId: void 0
4801
+ };
4802
+ if (!mirrorPresent || slotId === 0) {
4803
+ const allocated = this.bufferPool.alloc(requiredBytes);
4804
+ if (!allocated.ok) return allocated;
4805
+ preparation.reservedSlotId = allocated.value.id;
4806
+ } else if (this.bufferPool.view(slotId).byteLength < requiredBytes) {
4807
+ const grown = this.bufferPool.grow(slotId, requiredBytes);
4808
+ if (!grown.ok) return grown;
4809
+ }
4810
+ return ok(preparation);
4811
+ }
4812
+ /** Release a target-capacity reservation that did not reach commit. */
4813
+ releaseRelationshipPreparation(preparation) {
4814
+ if (preparation === void 0 || preparation.reservedSlotId === void 0) return;
4815
+ const slotId = preparation.reservedSlotId;
4816
+ this.bufferPool.release(slotId);
4817
+ preparation.reservedSlotId = void 0;
4818
+ }
4819
+ /** Install a reserved slot after the target mirror archetype exists. */
4820
+ installRelationshipPreparation(preparation) {
4821
+ const record = this.lookupAlive(
4822
+ preparation.target,
4823
+ "relationship-capacity",
4824
+ preparation.mirror.name
4825
+ );
4826
+ if (!record.ok) return record;
4827
+ const arch = this.graph.archetypes[record.value.archetypeId];
4828
+ if (arch === void 0) {
4829
+ return err(
4830
+ new ComponentNotPresentError(preparation.target, preparation.mirror.name)
4831
+ );
4832
+ }
4833
+ const fieldCols = this.table(arch).storage.get(componentId(preparation.mirror))?.fields;
4834
+ const column = fieldCols?.get(preparation.fieldName);
4835
+ if (column === void 0) {
4836
+ return err(
4837
+ new ComponentNotPresentError(preparation.target, preparation.mirror.name)
4614
4838
  );
4615
- if (!added.ok) return added;
4616
4839
  }
4617
- const length = this.relationshipTargetEntries(component, target).length;
4618
- return this.ensureArrayCapacity(target, mirror, role.targetField, length + 1);
4840
+ const slotId = preparation.reservedSlotId;
4841
+ if (slotId !== void 0) {
4842
+ column.view[this.tableRow(record.value)] = slotId;
4843
+ preparation.reservedSlotId = void 0;
4844
+ }
4845
+ return ok(void 0);
4619
4846
  }
4620
4847
  /** Append `holder` to the materialized target list. */
4621
- relationshipOnInsert(holder, component, value) {
4848
+ relationshipOnInsert(holder, component, value, preparation) {
4622
4849
  const role = relationshipRole(component);
4623
4850
  if (role?.kind !== "source") return ok(void 0);
4624
4851
  const target = this.relationshipTargetEntity(component, value);
4625
4852
  if (target === null) return ok(void 0);
4626
4853
  const mirror = relationshipMirror(component);
4627
4854
  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;
4855
+ let prepared = preparation;
4856
+ if (prepared === void 0) {
4857
+ const preparedResult = this.prepareRelationshipInsert(component, value);
4858
+ if (!preparedResult.ok) return preparedResult;
4859
+ prepared = preparedResult.value;
4632
4860
  }
4861
+ if (prepared === void 0) return ok(void 0);
4633
4862
  const targetSlot = entityIndex(target);
4634
4863
  const targetRec = this.records[targetSlot];
4635
- if (!this.recordIsLive(targetRec, entityGeneration(target))) return ok(void 0);
4864
+ const actualGeneration = targetRec?.generation ?? -1;
4865
+ if (!this.recordIsLive(targetRec, entityGeneration(target))) {
4866
+ return err(
4867
+ new StaleEntityError(target, targetSlot, entityGeneration(target), {
4868
+ operation: "relationship-insert",
4869
+ component: component.name,
4870
+ expectedGeneration: entityGeneration(target),
4871
+ actualGeneration
4872
+ })
4873
+ );
4874
+ }
4636
4875
  const targetArch = this.graph.archetypes[targetRec.archetypeId];
4637
4876
  const mirrorLocalId = componentId(mirror);
4638
4877
  const hasMirror = targetArch?.components.some((component2) => componentId(component2) === mirrorLocalId) ?? false;
@@ -4643,9 +4882,19 @@ var WorldComponentAccess = class {
4643
4882
  component: mirror,
4644
4883
  data: {}
4645
4884
  },
4885
+ true,
4886
+ false,
4646
4887
  true
4647
4888
  );
4648
- if (!added.ok) return added;
4889
+ if (!added.ok) {
4890
+ this.releaseRelationshipPreparation(prepared);
4891
+ return added;
4892
+ }
4893
+ }
4894
+ const installed = this.installRelationshipPreparation(prepared);
4895
+ if (!installed.ok) {
4896
+ this.releaseRelationshipPreparation(prepared);
4897
+ return installed;
4649
4898
  }
4650
4899
  const targetEntries = this.relationshipTargetEntries(component, target);
4651
4900
  const slot = targetEntries.length;
@@ -4655,7 +4904,10 @@ var WorldComponentAccess = class {
4655
4904
  role.targetField,
4656
4905
  holder
4657
4906
  );
4658
- if (!mirrored.ok) return mirrored;
4907
+ if (!mirrored.ok) {
4908
+ this.releaseRelationshipPreparation(prepared);
4909
+ return mirrored;
4910
+ }
4659
4911
  this.relationshipIndex(component)?.attach(holder, target, slot);
4660
4912
  return ok(void 0);
4661
4913
  }
@@ -4688,15 +4940,16 @@ var WorldComponentAccess = class {
4688
4940
  /**
4689
4941
  * Read component data from an entity.
4690
4942
  *
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.
4943
+ * **Public array contract:** relationship target `array<entity>` fields are
4944
+ * detached `Uint32Array` copies. Mutating that returned array cannot alter
4945
+ * the materialized target, relationship index, or source. Other array
4946
+ * fields retain the existing transient view contract: fixed-capacity
4947
+ * `array<T,N>` and `buffer<N>` fields alias the archetype column buffer
4948
+ * directly, while variable managed arrays alias their BufferPool slot. Those
4949
+ * views are valid only until the next structural change (`spawn` /
4950
+ * `despawn` / `addComponent` / `removeComponent`); callers must re-fetch
4951
+ * `world.get(e, C)` on every access. Internal owners use `readRow` and
4952
+ * `_getArrayView` directly and retain zero-copy access.
4700
4953
  *
4701
4954
  * @returns `Result<ShapeOf<S>, EcsError>` —
4702
4955
  * `ok(ShapeOf<S>)` on success;
@@ -4736,7 +4989,13 @@ var WorldComponentAccess = class {
4736
4989
  if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
4737
4990
  return err(new ComponentNotPresentError(entity, component.name));
4738
4991
  }
4739
- return ok(this.storage.readRow(arch, component, this.tableRow(rec)));
4992
+ const value = this.storage.readRow(arch, component, this.tableRow(rec));
4993
+ const role = relationshipRole(component);
4994
+ if (role?.kind === "target") {
4995
+ const targetValue = value;
4996
+ targetValue[role.targetField] = detachRelationshipTargetArray(targetValue[role.targetField]);
4997
+ }
4998
+ return ok(value);
4740
4999
  }
4741
5000
  /**
4742
5001
  * Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
@@ -4770,9 +5029,10 @@ var WorldComponentAccess = class {
4770
5029
  * Returns `undefined` when the entity is dead, the component is absent, the
4771
5030
  * field does not exist, or the field is not an `array<...>` column.
4772
5031
  *
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.
5032
+ * @internal Engine-internal fast path; AI users read public component values
5033
+ * through `world.get`. This accessor is the zero-materialization route the
5034
+ * propagate kernel, relationship owner, and render walk use; it bypasses
5035
+ * the detached public relationship-target snapshot.
4776
5036
  */
4777
5037
  _getArrayView(entity, component, fieldName) {
4778
5038
  const record = this.lookupAlive(entity, "_getArrayView", component.name);
@@ -4782,6 +5042,109 @@ var WorldComponentAccess = class {
4782
5042
  if (!arch) return void 0;
4783
5043
  return this.storage.readArrayView(arch, component, this.tableRow(rec), fieldName);
4784
5044
  }
5045
+ /** Internal zero-materialisation read for ECS-owned relationship lists. */
5046
+ _getArrayLength(entity, component, fieldName) {
5047
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5048
+ if (!record.ok) return void 0;
5049
+ const arch = this.graph.archetypes[record.value.archetypeId];
5050
+ if (arch === void 0) return void 0;
5051
+ return this.storage.readArrayLength(arch, component, this.tableRow(record.value), fieldName);
5052
+ }
5053
+ /** Internal zero-materialisation read for one ECS-owned array element. */
5054
+ _getArrayElement(entity, component, fieldName, index) {
5055
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5056
+ if (!record.ok) return void 0;
5057
+ const arch = this.graph.archetypes[record.value.archetypeId];
5058
+ if (arch === void 0) return void 0;
5059
+ return this.storage.readArrayElement(
5060
+ arch,
5061
+ component,
5062
+ this.tableRow(record.value),
5063
+ fieldName,
5064
+ index
5065
+ );
5066
+ }
5067
+ /** Internal scalar-column read used by parent-first hierarchy traversal. */
5068
+ _getFieldValue(entity, component, fieldName) {
5069
+ const record = this.lookupAlive(entity, "relationship-read", component.name);
5070
+ if (!record.ok) return void 0;
5071
+ const arch = this.graph.archetypes[record.value.archetypeId];
5072
+ if (arch === void 0) return void 0;
5073
+ return this.storage.readFieldValue(arch, component, this.tableRow(record.value), fieldName);
5074
+ }
5075
+ /**
5076
+ * Converge one writable relationship source mutation through the source
5077
+ * owner. The source scalar and its materialized target list are committed as
5078
+ * one operation; no caller receives a raw source column view that could
5079
+ * bypass the mirror/index maintenance.
5080
+ */
5081
+ setRelationshipSource(entity, component, value, record, arch, markChanged) {
5082
+ const role = relationshipRole(component);
5083
+ if (role?.kind !== "source") return ok(void 0);
5084
+ const row = this.tableRow(record);
5085
+ const current = this.storage.readRow(arch, component, row);
5086
+ const valuePreflight = this.preflightComponentFieldValues(entity, {
5087
+ component,
5088
+ data: value
5089
+ });
5090
+ if (!valuePreflight.ok) return valuePreflight;
5091
+ const merged = { ...current, ...value };
5092
+ const enumError = validateEnumFieldValues(component, merged, entity);
5093
+ if (enumError !== null) return err(enumError);
5094
+ const oldTarget = this.relationshipTargetEntity(component, current);
5095
+ const target = this.relationshipTargetEntity(component, merged);
5096
+ if (target !== null) {
5097
+ const targetRecord = this.records[entityIndex(target)];
5098
+ const actualGeneration = targetRecord?.generation ?? -1;
5099
+ if (!this.recordIsLive(targetRecord, entityGeneration(target))) {
5100
+ return err(
5101
+ new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
5102
+ operation: "relationship-insert",
5103
+ component: component.name,
5104
+ expectedGeneration: entityGeneration(target),
5105
+ actualGeneration
5106
+ })
5107
+ );
5108
+ }
5109
+ const roleAllowsSelf = role.allowSelf;
5110
+ if (entity === target && !roleAllowsSelf) {
5111
+ return err(
5112
+ new RelationshipSelfCycleError(component.name, entity, target)
5113
+ );
5114
+ }
5115
+ const cycleHit = entity === target && roleAllowsSelf ? null : this.relationshipCycleHit(component, target, entity);
5116
+ if (cycleHit !== null) {
5117
+ return err(
5118
+ new RelationshipSelfCycleError(component.name, entity, cycleHit)
5119
+ );
5120
+ }
5121
+ }
5122
+ if (oldTarget !== target) {
5123
+ const prepared = this.prepareRelationshipInsert(component, merged);
5124
+ if (!prepared.ok) return prepared;
5125
+ const preparation = prepared.value;
5126
+ if (oldTarget !== null) {
5127
+ const detached = this.relationshipOnRemove(entity, component, current);
5128
+ if (!detached.ok) {
5129
+ this.releaseRelationshipPreparation(preparation);
5130
+ return detached;
5131
+ }
5132
+ }
5133
+ const attached = this.relationshipOnInsert(entity, component, merged, preparation);
5134
+ if (!attached.ok) {
5135
+ this.releaseRelationshipPreparation(preparation);
5136
+ return attached;
5137
+ }
5138
+ }
5139
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
5140
+ const sourceColumn = fieldCols?.get(role.sourceField);
5141
+ if (sourceColumn === void 0) {
5142
+ return err(new ComponentNotPresentError(entity, component.name));
5143
+ }
5144
+ sourceColumn.view[row] = target === null ? ENTITY_NULL_RAW : target;
5145
+ if (markChanged) this.markComponentChanged(entity, component);
5146
+ return ok(void 0);
5147
+ }
4785
5148
  /**
4786
5149
  * Write (partial) component data to an entity.
4787
5150
  *
@@ -4820,6 +5183,20 @@ var WorldComponentAccess = class {
4820
5183
  if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
4821
5184
  return err(new ComponentNotPresentError(entity, component.name));
4822
5185
  }
5186
+ const role = relationshipRole(component);
5187
+ if (role?.kind === "target") {
5188
+ return err(new RelationshipTargetReadonlyError(component.name, "set"));
5189
+ }
5190
+ if (role?.kind === "source") {
5191
+ return this.setRelationshipSource(
5192
+ entity,
5193
+ component,
5194
+ value,
5195
+ rec,
5196
+ arch,
5197
+ markChanged
5198
+ );
5199
+ }
4823
5200
  const valuePreflight = this.preflightComponentFieldValues(entity, {
4824
5201
  component,
4825
5202
  data: value
@@ -4986,51 +5363,6 @@ var WorldComponentAccess = class {
4986
5363
  this.markComponentChanged(entity, component);
4987
5364
  return ok(void 0);
4988
5365
  }
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
5366
  /**
5035
5367
  * Remove one variable-array element at a known slot. Relationship holders
5036
5368
  * supply the slot from their backpointer, so this is O(1) and never scans
@@ -5098,7 +5430,7 @@ var WorldComponentAccess = class {
5098
5430
  * (lazy mirror create or exclusive reparent).
5099
5431
  * @internal
5100
5432
  */
5101
- _addComponentCore(entity, componentData, internal, resolveRequirements = true) {
5433
+ _addComponentCore(entity, componentData, internal, resolveRequirements = true, skipVariableArrayInitialization = false) {
5102
5434
  const record = this.lookupAlive(entity, "addComponent", componentData.component.name);
5103
5435
  if (!record.ok) return record;
5104
5436
  const rec = record.value;
@@ -5144,6 +5476,19 @@ var WorldComponentAccess = class {
5144
5476
  if (enumErr !== null) {
5145
5477
  return err(enumErr);
5146
5478
  }
5479
+ const localId = componentId(componentData.component);
5480
+ let relationshipPreparation;
5481
+ const componentAlreadyPresent = srcArch.components.some(
5482
+ (candidate) => componentId(candidate) === localId
5483
+ );
5484
+ if (!internal && !componentAlreadyPresent && relationshipRole(componentData.component)?.kind === "source") {
5485
+ const prepared = this.prepareRelationshipInsert(
5486
+ componentData.component,
5487
+ filled
5488
+ );
5489
+ if (!prepared.ok) return prepared;
5490
+ relationshipPreparation = prepared.value;
5491
+ }
5147
5492
  if (resolveRequirements) {
5148
5493
  const required = expandComponentRequirements([componentData]).slice(1);
5149
5494
  for (const requirement of required) {
@@ -5153,9 +5498,13 @@ var WorldComponentAccess = class {
5153
5498
  continue;
5154
5499
  }
5155
5500
  const added = this._addComponentCore(entity, requirement, internal, false);
5156
- if (!added.ok) return added;
5501
+ if (!added.ok) {
5502
+ this.releaseRelationshipPreparation(relationshipPreparation);
5503
+ return added;
5504
+ }
5157
5505
  srcArch = this.graph.archetypes[rec.archetypeId];
5158
5506
  if (!srcArch) {
5507
+ this.releaseRelationshipPreparation(relationshipPreparation);
5159
5508
  return err(
5160
5509
  new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
5161
5510
  operation: "addComponent",
@@ -5167,32 +5516,21 @@ var WorldComponentAccess = class {
5167
5516
  }
5168
5517
  }
5169
5518
  }
5170
- const localId = componentId(componentData.component);
5171
5519
  if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
5520
+ this.releaseRelationshipPreparation(relationshipPreparation);
5172
5521
  const role = relationshipRole(componentData.component);
5173
5522
  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(
5523
+ return this.setRelationshipSource(
5180
5524
  entity,
5181
5525
  componentData.component,
5182
- false
5526
+ filled,
5527
+ rec,
5528
+ srcArch,
5529
+ true
5183
5530
  );
5184
- if (!removeR.ok) return removeR;
5185
- return this._addComponentCore(entity, componentData, false);
5186
5531
  }
5187
5532
  return err(new ComponentAlreadyPresentError(entity, componentData.component.name));
5188
5533
  }
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
5534
  const targetArch = getAddEdge(
5197
5535
  this.graph,
5198
5536
  srcArch,
@@ -5209,7 +5547,8 @@ var WorldComponentAccess = class {
5209
5547
  targetArch,
5210
5548
  componentData.component,
5211
5549
  this.tableRow(rec),
5212
- filled
5550
+ filled,
5551
+ skipVariableArrayInitialization ? { skipVariableArrayInitialization: true } : void 0
5213
5552
  );
5214
5553
  }
5215
5554
  this.markComponentAdded(entity, componentData.component);
@@ -5217,9 +5556,13 @@ var WorldComponentAccess = class {
5217
5556
  const relationshipResult = this.relationshipOnInsert(
5218
5557
  entity,
5219
5558
  componentData.component,
5220
- filled
5559
+ filled,
5560
+ relationshipPreparation
5221
5561
  );
5222
- if (!relationshipResult.ok) return relationshipResult;
5562
+ if (!relationshipResult.ok) {
5563
+ this.releaseRelationshipPreparation(relationshipPreparation);
5564
+ return relationshipResult;
5565
+ }
5223
5566
  }
5224
5567
  this.markStructureChanged();
5225
5568
  this.state.recordStructuralEvidence({
@@ -5347,6 +5690,35 @@ var WorldComponentAccess = class {
5347
5690
  const slot = entityIndex(entity);
5348
5691
  const record = this.records[slot];
5349
5692
  if (!record || record.archetypeId !== -1) return ok(void 0);
5693
+ const relationshipPreparations = [];
5694
+ for (let index = 0; index < componentDatas.length; index += 1) {
5695
+ const componentData = componentDatas[index];
5696
+ if (componentData === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
5697
+ continue;
5698
+ }
5699
+ const filled = fillComponentDefaults(
5700
+ componentData.component,
5701
+ componentData.data
5702
+ );
5703
+ const target = this.relationshipTargetEntity(
5704
+ componentData.component,
5705
+ filled
5706
+ );
5707
+ const targetRecord = target === null ? void 0 : this.records[entityIndex(target)];
5708
+ if (target !== null && targetRecord !== void 0 && this.recordIsLive(targetRecord, entityGeneration(target))) {
5709
+ const prepared = this.prepareRelationshipInsert(
5710
+ componentData.component,
5711
+ filled
5712
+ );
5713
+ if (!prepared.ok) {
5714
+ for (const reservation of relationshipPreparations) {
5715
+ this.releaseRelationshipPreparation(reservation);
5716
+ }
5717
+ return prepared;
5718
+ }
5719
+ relationshipPreparations[index] = prepared.value;
5720
+ }
5721
+ }
5350
5722
  const componentIds = componentDatas.map((cd) => componentId(cd.component));
5351
5723
  const components = componentDatas.map((cd) => cd.component);
5352
5724
  const arch = getOrCreateArchetype(this.graph, componentIds, components);
@@ -5364,15 +5736,21 @@ var WorldComponentAccess = class {
5364
5736
  this.storage.writeRow(arch, cd.component, tableRow2, filled);
5365
5737
  }
5366
5738
  this.storage.writeEntitySelf(arch, tableRow2, entity);
5367
- for (const cd of componentDatas) {
5739
+ for (let index = 0; index < componentDatas.length; index += 1) {
5740
+ const cd = componentDatas[index];
5741
+ if (cd === void 0) continue;
5368
5742
  if (relationshipRole(cd.component)?.kind === "source") {
5369
5743
  const filled = fillComponentDefaults(cd.component, cd.data);
5370
5744
  const relationshipResult = this.relationshipOnInsert(
5371
5745
  entity,
5372
5746
  cd.component,
5373
- filled
5747
+ filled,
5748
+ relationshipPreparations[index]
5374
5749
  );
5375
5750
  if (!relationshipResult.ok) {
5751
+ for (const reservation of relationshipPreparations) {
5752
+ this.releaseRelationshipPreparation(reservation);
5753
+ }
5376
5754
  return relationshipResult;
5377
5755
  }
5378
5756
  }
@@ -6137,6 +6515,27 @@ function spawnCore(world, componentDatas, internal) {
6137
6515
  if (enumErr !== null) return err(enumErr);
6138
6516
  filledData.push(filled);
6139
6517
  }
6518
+ const relationshipPreparations = [];
6519
+ if (!internal) {
6520
+ for (let index = 0; index < componentDatas.length; index += 1) {
6521
+ const componentData = componentDatas[index];
6522
+ const value = filledData[index];
6523
+ if (componentData === void 0 || value === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
6524
+ continue;
6525
+ }
6526
+ const prepared = world[worldInternal].prepareRelationshipInsert(
6527
+ componentData.component,
6528
+ value
6529
+ );
6530
+ if (!prepared.ok) {
6531
+ for (const reservation of relationshipPreparations) {
6532
+ world[worldInternal].releaseRelationshipPreparation(reservation);
6533
+ }
6534
+ return prepared;
6535
+ }
6536
+ relationshipPreparations[index] = prepared.value;
6537
+ }
6538
+ }
6140
6539
  const indexSlot = world[worldInternal].allocateIndex();
6141
6540
  const record = world[worldInternal].getRecords()[indexSlot];
6142
6541
  if (record === void 0)
@@ -6172,9 +6571,15 @@ function spawnCore(world, componentDatas, internal) {
6172
6571
  const relation = world[worldInternal].relationshipOnInsert(
6173
6572
  spawnedEntity,
6174
6573
  cd.component,
6175
- filled
6574
+ filled,
6575
+ relationshipPreparations[i]
6176
6576
  );
6177
- if (!relation.ok) return relation;
6577
+ if (!relation.ok) {
6578
+ for (const reservation of relationshipPreparations) {
6579
+ world[worldInternal].releaseRelationshipPreparation(reservation);
6580
+ }
6581
+ return relation;
6582
+ }
6178
6583
  }
6179
6584
  }
6180
6585
  world[worldInternal].markStructureChanged();
@@ -6248,7 +6653,8 @@ function despawnCore(world, entity, internal) {
6248
6653
  }
6249
6654
  function worldAddChild(world, parent, child, component, data) {
6250
6655
  const holderComp = component;
6251
- if (relationshipRole(holderComp)?.kind !== "source") {
6656
+ const role = relationshipRole(holderComp);
6657
+ if (role?.kind !== "source") {
6252
6658
  return err(new ComponentNotPresentError(child, component.name));
6253
6659
  }
6254
6660
  const parentSlot = entityIndex(parent);
@@ -6277,7 +6683,6 @@ function worldAddChild(world, parent, child, component, data) {
6277
6683
  })
6278
6684
  );
6279
6685
  }
6280
- const role = relationshipRole(holderComp);
6281
6686
  if (child === parent && !(role?.kind === "source" && role.allowSelf)) {
6282
6687
  return err(new RelationshipSelfCycleError(component.name, child, child));
6283
6688
  }
@@ -6328,10 +6733,10 @@ function worldRemoveChild(world, parent, child, component) {
6328
6733
  }
6329
6734
  function worldReparent(world, child, newParent, component, data) {
6330
6735
  const holderComp = component;
6331
- if (relationshipRole(holderComp)?.kind !== "source") {
6736
+ const role = relationshipRole(holderComp);
6737
+ if (role?.kind !== "source") {
6332
6738
  return err(new ComponentNotPresentError(child, component.name));
6333
6739
  }
6334
- const role = relationshipRole(holderComp);
6335
6740
  if (child === newParent && !(role?.kind === "source" && role.allowSelf)) {
6336
6741
  return err(
6337
6742
  new RelationshipSelfCycleError(component.name, child, newParent)
@@ -6361,11 +6766,14 @@ function worldReparent(world, child, newParent, component, data) {
6361
6766
  })
6362
6767
  );
6363
6768
  }
6364
- if (childArch.components.some((component2) => componentId(component2) === componentId(holderComp))) {
6365
- const removeResult = world.removeComponent(child, component);
6366
- if (!removeResult.ok) return removeResult;
6769
+ const payload = {
6770
+ ...data,
6771
+ [role.sourceField]: newParent
6772
+ };
6773
+ if (childArch.components.some((candidate) => componentId(candidate) === componentId(holderComp))) {
6774
+ return world.set(child, component, payload);
6367
6775
  }
6368
- return world.addComponent(child, { component, data });
6776
+ return world.addComponent(child, { component, data: payload });
6369
6777
  }
6370
6778
  function worldIterAncestors(world, entity) {
6371
6779
  return {
@@ -6986,6 +7394,9 @@ var World = class {
6986
7394
  cancelPendingEntity: this.internalcancelPendingEntity.bind(this),
6987
7395
  despawnCore: this.internaldespawnCore.bind(this),
6988
7396
  getArrayView: this.internalgetArrayView.bind(this),
7397
+ getArrayLength: this.internalgetArrayLength.bind(this),
7398
+ getArrayElement: this.internalgetArrayElement.bind(this),
7399
+ getFieldValue: this.internalgetFieldValue.bind(this),
6989
7400
  getBufferPool: this.internalgetBufferPool.bind(this),
6990
7401
  getClockWriter: this.internalgetClockWriter.bind(this),
6991
7402
  getComponentChange: this.internalgetComponentChange.bind(this),
@@ -7016,6 +7427,7 @@ var World = class {
7016
7427
  materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
7017
7428
  nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
7018
7429
  poisonExecution: this.internalpoisonExecution.bind(this),
7430
+ prepareRelationshipInsert: this.internalprepareRelationshipInsert.bind(this),
7019
7431
  publishDerivedRange: this.internalpublishDerivedRange.bind(this),
7020
7432
  preflightComponentData: this.internalpreflightComponentData.bind(this),
7021
7433
  readRow: this.internalreadRow.bind(this),
@@ -7024,6 +7436,7 @@ var World = class {
7024
7436
  relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
7025
7437
  relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
7026
7438
  releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
7439
+ releaseRelationshipPreparation: this.internalreleaseRelationshipPreparation.bind(this),
7027
7440
  removeComponentCore: this.internalremoveComponentCore.bind(this),
7028
7441
  routeError: this.internalrouteError.bind(this),
7029
7442
  restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
@@ -7175,7 +7588,12 @@ var World = class {
7175
7588
  }
7176
7589
  /** Query facade write after the facade has already marked evidence. */
7177
7590
  internalsetQueryRow(entity, component, value) {
7178
- return this.componentAccess.set(entity, component, value, false);
7591
+ return this.componentAccess.set(
7592
+ entity,
7593
+ component,
7594
+ value,
7595
+ relationshipRole(component)?.kind === "source"
7596
+ );
7179
7597
  }
7180
7598
  /** Query facade read that does not re-enter the public World API. */
7181
7599
  internalgetQueryRow(entity, component) {
@@ -7523,6 +7941,15 @@ var World = class {
7523
7941
  internalgetArrayView(entity, component, fieldName) {
7524
7942
  return this.componentAccess._getArrayView(entity, component, fieldName);
7525
7943
  }
7944
+ internalgetArrayLength(entity, component, fieldName) {
7945
+ return this.componentAccess._getArrayLength(entity, component, fieldName);
7946
+ }
7947
+ internalgetArrayElement(entity, component, fieldName, index) {
7948
+ return this.componentAccess._getArrayElement(entity, component, fieldName, index);
7949
+ }
7950
+ internalgetFieldValue(entity, component, fieldName) {
7951
+ return this.componentAccess._getFieldValue(entity, component, fieldName);
7952
+ }
7526
7953
  set(entity, component, value) {
7527
7954
  if (isRelationshipTarget(component)) return this.relationshipTargetWriteError(component, "set");
7528
7955
  return this.componentAccess.set(entity, component, value);
@@ -7591,8 +8018,16 @@ var World = class {
7591
8018
  this.componentAccess.releaseManagedRefsOnRow(a, c, r);
7592
8019
  }
7593
8020
  /** */
7594
- internalrelationshipOnInsert(h, c, v) {
7595
- return this.componentAccess.relationshipOnInsert(h, c, v);
8021
+ internalrelationshipOnInsert(h, c, v, preparation) {
8022
+ return this.componentAccess.relationshipOnInsert(h, c, v, preparation);
8023
+ }
8024
+ /** */
8025
+ internalprepareRelationshipInsert(c, v) {
8026
+ return this.componentAccess.prepareRelationshipInsert(c, v);
8027
+ }
8028
+ /** */
8029
+ internalreleaseRelationshipPreparation(preparation) {
8030
+ this.componentAccess.releaseRelationshipPreparation(preparation);
7596
8031
  }
7597
8032
  /** */
7598
8033
  internalrelationshipOnRemove(h, c, v) {