@forgeax/engine-ecs 0.1.21 → 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.
- package/README.md +83 -17
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts +2 -0
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts.map +1 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts +2 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-version-surface.test.d.ts +2 -0
- package/dist/__tests__/component-version-surface.test.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts.map +1 -0
- package/dist/component-schema.d.ts +17 -0
- package/dist/component-schema.d.ts.map +1 -1
- package/dist/component.d.ts +16 -7
- package/dist/component.d.ts.map +1 -1
- package/dist/errors/query-and-component-errors.d.ts +1 -1
- package/dist/errors/query-and-component-errors.d.ts.map +1 -1
- package/dist/errors/validation-errors.d.ts +24 -0
- package/dist/errors/validation-errors.d.ts.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/externalization/index.mjs.map +1 -1
- package/dist/index.mjs +977 -255
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +18 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +20 -2
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.d.ts +27 -52
- package/dist/projection/index.d.ts.map +1 -1
- package/dist/projection/index.mjs +66 -76
- package/dist/projection/index.mjs.map +1 -1
- package/dist/query/derived-range-writer.d.ts +62 -0
- package/dist/query/derived-range-writer.d.ts.map +1 -0
- package/dist/query/query.d.ts +1 -1
- package/dist/query/query.d.ts.map +1 -1
- package/dist/relationship-index.d.ts +8 -0
- package/dist/relationship-index.d.ts.map +1 -1
- package/dist/shared-ref-store.d.ts +5 -9
- package/dist/shared-ref-store.d.ts.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/storage/change-detection.d.ts +1 -3
- package/dist/storage/change-detection.d.ts.map +1 -1
- package/dist/storage/structural-evidence.d.ts +30 -0
- package/dist/storage/structural-evidence.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +65 -19
- package/dist/world-component-access.d.ts.map +1 -1
- package/dist/world-component-storage.d.ts +26 -10
- package/dist/world-component-storage.d.ts.map +1 -1
- package/dist/world-core.d.ts +2 -2
- package/dist/world-core.d.ts.map +1 -1
- package/dist/world-entity-lifecycle.d.ts.map +1 -1
- package/dist/world-internal.d.ts +1 -1
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world.d.ts +15 -14
- package/dist/world.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/changed-block-summary.candidate.test.ts +57 -0
- package/src/__tests__/component-requirements.unit.test.ts +76 -0
- package/src/__tests__/component-version-surface.test.ts +45 -0
- package/src/__tests__/derived-range-writer.contract.test-d.ts +35 -0
- package/src/__tests__/derived-range-writer.contract.test.ts +339 -0
- package/src/__tests__/errors.unit.test.ts +21 -5
- package/src/__tests__/externalization-render-read-lease.test-d.ts +1 -1
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +15 -15
- package/src/__tests__/query-storage-trends.perf.test.ts +19 -0
- package/src/__tests__/query.unit.test.ts +10 -6
- package/src/__tests__/relationship-index.test.ts +147 -1
- package/src/__tests__/shared-ref-store.unit.test.ts +3 -8
- package/src/__tests__/structural-evidence.contract.test-d.ts +6 -0
- package/src/__tests__/structural-evidence.contract.test.ts +49 -0
- package/src/component-schema.ts +51 -0
- package/src/component.ts +21 -8
- package/src/errors/query-and-component-errors.ts +4 -1
- package/src/errors/validation-errors.ts +30 -0
- package/src/errors.ts +29 -5
- package/src/internal.ts +59 -0
- package/src/projection/index.ts +68 -149
- package/src/query/derived-range-writer.ts +443 -0
- package/src/query/query.ts +115 -16
- package/src/relationship-index.ts +13 -1
- package/src/shared-ref-store.ts +16 -24
- package/src/storage/change-detection.ts +0 -15
- package/src/storage/structural-evidence.ts +64 -0
- package/src/world-component-access.ts +479 -130
- package/src/world-component-storage.ts +104 -18
- package/src/world-core.ts +2 -2
- package/src/world-entity-lifecycle.ts +59 -10
- package/src/world-internal.ts +10 -5
- package/src/world.ts +97 -53
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/world-change-journal.unit.test.d.ts +0 -2
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +0 -1
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +0 -2
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +0 -1
- package/dist/world-change-journal.d.ts +0 -34
- package/dist/world-change-journal.d.ts.map +0 -1
- package/src/__tests__/world-change-journal.unit.test.ts +0 -97
- package/src/__tests__/world-internal-identity.unit.test.ts +0 -17
- package/src/world-change-journal.ts +0 -84
|
@@ -114,6 +114,79 @@ export class ComponentStorage {
|
|
|
114
114
|
return reinterpretSlotBytes(liveBytes, arrayMeta.elementType, elementCount);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Read the live length of an array field without materialising a typed view.
|
|
119
|
+
* Relationship consumers use this narrow storage seam for hot reverse-list
|
|
120
|
+
* walks; the target array remains owned by the ECS column and no snapshot is
|
|
121
|
+
* created per entity.
|
|
122
|
+
*/
|
|
123
|
+
readArrayLength(
|
|
124
|
+
arch: Archetype,
|
|
125
|
+
component: Component,
|
|
126
|
+
row: number,
|
|
127
|
+
fieldName: string,
|
|
128
|
+
): number | undefined {
|
|
129
|
+
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
130
|
+
if (fieldCols === undefined) return undefined;
|
|
131
|
+
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
132
|
+
if (arrayMeta === undefined) return undefined;
|
|
133
|
+
if (arrayMeta.length !== undefined) return arrayMeta.length;
|
|
134
|
+
const count = fieldCols.get(arrayCountColumnName(fieldName))?.view[row];
|
|
135
|
+
return typeof count === 'number' ? count : 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read one array element directly from its backing column/BufferPool slot.
|
|
140
|
+
* The relationship target vocabulary is `array<entity>`, so its hot path
|
|
141
|
+
* decodes the packed u32 in-place and does not allocate a TypedArray view.
|
|
142
|
+
*/
|
|
143
|
+
readArrayElement(
|
|
144
|
+
arch: Archetype,
|
|
145
|
+
component: Component,
|
|
146
|
+
row: number,
|
|
147
|
+
fieldName: string,
|
|
148
|
+
index: number,
|
|
149
|
+
): number | undefined {
|
|
150
|
+
if (!Number.isSafeInteger(index) || index < 0) return undefined;
|
|
151
|
+
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
152
|
+
if (fieldCols === undefined) return undefined;
|
|
153
|
+
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
154
|
+
if (arrayMeta === undefined) return undefined;
|
|
155
|
+
const length = this.readArrayLength(arch, component, row, fieldName);
|
|
156
|
+
if (length === undefined || index >= length) return undefined;
|
|
157
|
+
const col = fieldCols.get(fieldName);
|
|
158
|
+
if (col === undefined) return undefined;
|
|
159
|
+
if (arrayMeta.length !== undefined) {
|
|
160
|
+
return col.view[row * col.arity + index] as number;
|
|
161
|
+
}
|
|
162
|
+
const slotId = col.view[row] as number;
|
|
163
|
+
const bytes = this.bufferPool.view(slotId);
|
|
164
|
+
if (arrayMeta.elementType === 'entity') {
|
|
165
|
+
const byteOffset = index * 4;
|
|
166
|
+
if (byteOffset + 4 > bytes.byteLength) return undefined;
|
|
167
|
+
return (
|
|
168
|
+
((bytes[byteOffset] ?? 0) |
|
|
169
|
+
((bytes[byteOffset + 1] ?? 0) << 8) |
|
|
170
|
+
((bytes[byteOffset + 2] ?? 0) << 16) |
|
|
171
|
+
((bytes[byteOffset + 3] ?? 0) << 24)) >>>
|
|
172
|
+
0
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return this.readArrayElementAt(bytes, index, arrayMeta.elementType);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Read one scalar field without constructing a component shape. */
|
|
179
|
+
readFieldValue(
|
|
180
|
+
arch: Archetype,
|
|
181
|
+
component: Component,
|
|
182
|
+
row: number,
|
|
183
|
+
fieldName: string,
|
|
184
|
+
): number | undefined {
|
|
185
|
+
return this.table(arch).storage.get(componentId(component))?.fields.get(fieldName)?.view[row] as
|
|
186
|
+
| number
|
|
187
|
+
| undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
117
190
|
// ──────────────────────────────────────────────────────────────────────────
|
|
118
191
|
// Internal — archetype data read/write
|
|
119
192
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -175,10 +248,10 @@ export class ComponentStorage {
|
|
|
175
248
|
} else {
|
|
176
249
|
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
177
250
|
if (arrayMeta !== undefined) {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
251
|
+
// Internal row path: materialise a fresh TypedArray view each call
|
|
252
|
+
// without copying its backing bytes. Public World.get detaches a
|
|
253
|
+
// relationship target array after this read; ECS relationship and
|
|
254
|
+
// Scene owners use this readRow seam as zero-copy storage access.
|
|
182
255
|
(out as Record<string, unknown>)[fieldName] = this.materializeArrayView(
|
|
183
256
|
arch,
|
|
184
257
|
component,
|
|
@@ -215,6 +288,7 @@ export class ComponentStorage {
|
|
|
215
288
|
component: Component<string, S>,
|
|
216
289
|
row: number,
|
|
217
290
|
value: ShapeOf<S>,
|
|
291
|
+
options?: { readonly skipVariableArrayInitialization?: boolean },
|
|
218
292
|
): void {
|
|
219
293
|
const localId = componentId(component);
|
|
220
294
|
const fieldCols = this.table(arch).storage.get(localId)?.fields;
|
|
@@ -291,6 +365,16 @@ export class ComponentStorage {
|
|
|
291
365
|
} else {
|
|
292
366
|
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
293
367
|
if (arrayMeta !== undefined) {
|
|
368
|
+
if (options?.skipVariableArrayInitialization === true && arrayMeta.length === undefined) {
|
|
369
|
+
// Relationship preparation can reserve the first target slot
|
|
370
|
+
// before the mirror archetype is created. Leave the fresh row at
|
|
371
|
+
// the sentinel until the owner installs that reservation; this
|
|
372
|
+
// keeps an injected alloc failure out of the structural path.
|
|
373
|
+
col.view[row] = 0;
|
|
374
|
+
const countCol = fieldCols.get(arrayCountColumnName(fieldName));
|
|
375
|
+
if (countCol !== undefined) countCol.view[row] = 0;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
294
378
|
// M1 spawn path for array<T> / array<T,N> fields (D-3 double-
|
|
295
379
|
// column for variable; single column for fixed).
|
|
296
380
|
// Spawn path: no prior-slot release (fresh rows carry stale debris
|
|
@@ -810,20 +894,19 @@ export class ComponentStorage {
|
|
|
810
894
|
}
|
|
811
895
|
|
|
812
896
|
/**
|
|
813
|
-
* Materialise a fresh `TypedArray`
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
897
|
+
* Materialise a fresh `TypedArray` view for an `array<T,N>` / `array<T>`
|
|
898
|
+
* field at `row`. This is the internal row-storage path: the returned view
|
|
899
|
+
* aliases the live column or BufferPool slot. `WorldComponentAccess.get`
|
|
900
|
+
* detaches relationship target arrays at the public boundary; internal ECS
|
|
901
|
+
* relationship maintenance and Scene traversal keep this zero-copy path.
|
|
817
902
|
*
|
|
818
903
|
* **Transient view contract (feat-20260602):** for fixed `array<T,N>`
|
|
819
904
|
* columns the returned `TypedArray` aliases the inline column buffer
|
|
820
905
|
* directly (`col.view.subarray(row * arity, ...)`); for variable
|
|
821
906
|
* `array<T>` columns it aliases the live `BufferPool` slot bytes
|
|
822
|
-
* (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* and the implementation may switch to a copy in the future without
|
|
826
|
-
* breaking AI users who consume only `length` / index reads.
|
|
907
|
+
* (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both cases
|
|
908
|
+
* the view is valid only until the next structural change. Internal callers
|
|
909
|
+
* must not write through it except via the owner mutation helpers.
|
|
827
910
|
*
|
|
828
911
|
* For variable arrays the typed-array length matches the live count from
|
|
829
912
|
* the sidecar `<fieldName>:count` column; for fixed arrays it matches the
|
|
@@ -1011,11 +1094,14 @@ export class ComponentStorage {
|
|
|
1011
1094
|
|
|
1012
1095
|
/**
|
|
1013
1096
|
* Reinterpret a `BufferPool` slot's `Uint8Array` byte region as the typed
|
|
1014
|
-
* view for `elementType`, sliced to `elementCount` elements.
|
|
1015
|
-
*
|
|
1016
|
-
* aliases live slot bytes
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1097
|
+
* view for `elementType`, sliced to `elementCount` elements. This is the
|
|
1098
|
+
* internal materialization used by `readRow`/`materializeArrayView`: the
|
|
1099
|
+
* returned view aliases live slot bytes. `WorldComponentAccess.get` detaches
|
|
1100
|
+
* relationship-target `array<entity>` values at the public boundary; other
|
|
1101
|
+
* public array fields retain the existing transient live-view contract.
|
|
1102
|
+
* `_getArrayView` and other internal owners always retain zero-copy access.
|
|
1103
|
+
* `entity` element fields surface as `Uint32Array` (Entity packs slot+gen
|
|
1104
|
+
* into u32).
|
|
1019
1105
|
*/
|
|
1020
1106
|
/**
|
|
1021
1107
|
* Element-byte-width for a managed-array element type, read off the global
|
package/src/world-core.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { RelationshipIndex } from './relationship-index';
|
|
|
4
4
|
import type { SharedRefStore } from './shared-ref-store';
|
|
5
5
|
import { SharedRefStore as SharedRefStoreImpl } from './shared-ref-store';
|
|
6
6
|
import { type ArchetypeGraph, createArchetypeGraph } from './storage/archetype-graph';
|
|
7
|
-
import {
|
|
7
|
+
import { StructuralEvidenceRing } from './storage/structural-evidence';
|
|
8
8
|
import type { UniqueRefStore } from './unique-ref-store';
|
|
9
9
|
import { UniqueRefStore as UniqueRefStoreImpl } from './unique-ref-store';
|
|
10
10
|
import type { EntityRecord } from './world';
|
|
@@ -43,7 +43,7 @@ export class WorldCore {
|
|
|
43
43
|
readonly sharedRefs: SharedRefStore = new SharedRefStoreImpl();
|
|
44
44
|
readonly storage: WorldStorageCore;
|
|
45
45
|
readonly componentMutationEpochs: number[] = [];
|
|
46
|
-
readonly
|
|
46
|
+
readonly structuralEvidence = new StructuralEvidenceRing();
|
|
47
47
|
/** One packed reverse index per relationship source component. */
|
|
48
48
|
readonly relationshipIndexes = new Map<number, RelationshipIndex>();
|
|
49
49
|
mutationEpoch = 0;
|
|
@@ -8,6 +8,7 @@ import { err, isRetiredSlot, ok, pack, type Result } from '@forgeax/engine-types
|
|
|
8
8
|
import type { Component, ComponentSchema, InputShapeOf, ShapeOf } from './component';
|
|
9
9
|
import { componentId, componentSchema } from './component';
|
|
10
10
|
import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
|
|
11
|
+
import { expandComponentRequirements } from './component-schema';
|
|
11
12
|
import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
|
|
12
13
|
import { Entity } from './entity';
|
|
13
14
|
import {
|
|
@@ -48,6 +49,7 @@ export function spawnCore(
|
|
|
48
49
|
componentDatas: { component: Component; data: Partial<Record<string, unknown>> }[],
|
|
49
50
|
internal: boolean,
|
|
50
51
|
): Result<EntityHandle, EcsError> {
|
|
52
|
+
componentDatas = expandComponentRequirements(componentDatas);
|
|
51
53
|
const filledData: Record<string, unknown>[] = [];
|
|
52
54
|
for (const cd of componentDatas) {
|
|
53
55
|
const preflight = world[worldInternal].preflightComponentData(null, cd);
|
|
@@ -63,6 +65,34 @@ export function spawnCore(
|
|
|
63
65
|
if (enumErr !== null) return err(enumErr as unknown as EcsError);
|
|
64
66
|
filledData.push(filled as Record<string, unknown>);
|
|
65
67
|
}
|
|
68
|
+
// Reserve every relationship target before allocating the new source row.
|
|
69
|
+
// In particular, an injected BufferPool failure must not leave a source
|
|
70
|
+
// entity, mirror component, or mutation epoch behind.
|
|
71
|
+
const relationshipPreparations: unknown[] = [];
|
|
72
|
+
if (!internal) {
|
|
73
|
+
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
74
|
+
const componentData = componentDatas[index];
|
|
75
|
+
const value = filledData[index];
|
|
76
|
+
if (
|
|
77
|
+
componentData === undefined ||
|
|
78
|
+
value === undefined ||
|
|
79
|
+
relationshipRole(componentData.component as Component)?.kind !== 'source'
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const prepared = world[worldInternal].prepareRelationshipInsert(
|
|
84
|
+
componentData.component as Component,
|
|
85
|
+
value,
|
|
86
|
+
);
|
|
87
|
+
if (!prepared.ok) {
|
|
88
|
+
for (const reservation of relationshipPreparations) {
|
|
89
|
+
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
90
|
+
}
|
|
91
|
+
return prepared;
|
|
92
|
+
}
|
|
93
|
+
relationshipPreparations[index] = prepared.value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
66
96
|
const indexSlot = world[worldInternal].allocateIndex();
|
|
67
97
|
const record = world[worldInternal].getRecords()[indexSlot];
|
|
68
98
|
if (record === undefined)
|
|
@@ -99,11 +129,21 @@ export function spawnCore(
|
|
|
99
129
|
spawnedEntity,
|
|
100
130
|
cd.component as Component,
|
|
101
131
|
filled,
|
|
132
|
+
relationshipPreparations[i],
|
|
102
133
|
);
|
|
103
|
-
if (!relation.ok)
|
|
134
|
+
if (!relation.ok) {
|
|
135
|
+
for (const reservation of relationshipPreparations) {
|
|
136
|
+
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
137
|
+
}
|
|
138
|
+
return relation;
|
|
139
|
+
}
|
|
104
140
|
}
|
|
105
141
|
}
|
|
106
142
|
world[worldInternal].markStructureChanged();
|
|
143
|
+
world[worldInternal].recordStructuralEvidence({
|
|
144
|
+
kind: 'spawn',
|
|
145
|
+
entity: spawnedEntity,
|
|
146
|
+
});
|
|
107
147
|
return ok(spawnedEntity);
|
|
108
148
|
}
|
|
109
149
|
|
|
@@ -169,8 +209,11 @@ export function despawnCore(
|
|
|
169
209
|
}
|
|
170
210
|
}
|
|
171
211
|
}
|
|
212
|
+
world[worldInternal].recordStructuralEvidence({
|
|
213
|
+
kind: 'despawn',
|
|
214
|
+
entity,
|
|
215
|
+
});
|
|
172
216
|
if (record) {
|
|
173
|
-
world[worldInternal].removeEntityChanges(entity);
|
|
174
217
|
record.archetypeId = -1;
|
|
175
218
|
record.archetypeRow = -1;
|
|
176
219
|
record.generation += 1;
|
|
@@ -190,7 +233,8 @@ export function worldAddChild<S extends ComponentSchema>(
|
|
|
190
233
|
data: Partial<InputShapeOf<S>>,
|
|
191
234
|
): Result<void, EcsError> {
|
|
192
235
|
const holderComp = component as Component;
|
|
193
|
-
|
|
236
|
+
const role = relationshipRole(holderComp);
|
|
237
|
+
if (role?.kind !== 'source') {
|
|
194
238
|
return err(new ComponentNotPresentError(child as number, component.name));
|
|
195
239
|
}
|
|
196
240
|
|
|
@@ -222,7 +266,6 @@ export function worldAddChild<S extends ComponentSchema>(
|
|
|
222
266
|
);
|
|
223
267
|
}
|
|
224
268
|
|
|
225
|
-
const role = relationshipRole(holderComp);
|
|
226
269
|
if (child === parent && !(role?.kind === 'source' && role.allowSelf)) {
|
|
227
270
|
return err(new RelationshipSelfCycleError(component.name, child as number, child as number));
|
|
228
271
|
}
|
|
@@ -299,10 +342,10 @@ export function worldReparent<S extends ComponentSchema>(
|
|
|
299
342
|
data: Partial<InputShapeOf<S>>,
|
|
300
343
|
): Result<void, EcsError> {
|
|
301
344
|
const holderComp = component as Component;
|
|
302
|
-
|
|
345
|
+
const role = relationshipRole(holderComp);
|
|
346
|
+
if (role?.kind !== 'source') {
|
|
303
347
|
return err(new ComponentNotPresentError(child as number, component.name));
|
|
304
348
|
}
|
|
305
|
-
const role = relationshipRole(holderComp);
|
|
306
349
|
if (child === newParent && !(role?.kind === 'source' && role.allowSelf)) {
|
|
307
350
|
return err(
|
|
308
351
|
new RelationshipSelfCycleError(component.name, child as number, newParent as number),
|
|
@@ -339,13 +382,19 @@ export function worldReparent<S extends ComponentSchema>(
|
|
|
339
382
|
}),
|
|
340
383
|
);
|
|
341
384
|
}
|
|
385
|
+
const payload = {
|
|
386
|
+
...(data as Record<string, unknown>),
|
|
387
|
+
[role.sourceField]: newParent,
|
|
388
|
+
} as Partial<InputShapeOf<S>>;
|
|
342
389
|
if (
|
|
343
|
-
childArch.components.some((
|
|
390
|
+
childArch.components.some((candidate) => componentId(candidate) === componentId(holderComp))
|
|
344
391
|
) {
|
|
345
|
-
|
|
346
|
-
|
|
392
|
+
// Existing exclusive sources are updated through the same owner-level
|
|
393
|
+
// write barrier as `world.set`; remove+add would expose a partial mirror
|
|
394
|
+
// state and would invalidate unrelated query spans.
|
|
395
|
+
return world.set(child, component as never, payload as never);
|
|
347
396
|
}
|
|
348
|
-
return world.addComponent(child, { component, data });
|
|
397
|
+
return world.addComponent(child, { component, data: payload });
|
|
349
398
|
}
|
|
350
399
|
|
|
351
400
|
/** Iterate ancestors in child-to-root order while safely terminating corrupt cycles. */
|
package/src/world-internal.ts
CHANGED
|
@@ -22,11 +22,14 @@ type InternalName =
|
|
|
22
22
|
| 'cancelPendingEntity'
|
|
23
23
|
| 'despawnCore'
|
|
24
24
|
| 'getArrayView'
|
|
25
|
+
| 'getArrayLength'
|
|
26
|
+
| 'getArrayElement'
|
|
27
|
+
| 'getFieldValue'
|
|
25
28
|
| 'getBufferPool'
|
|
26
|
-
| 'getChangeCursor'
|
|
27
29
|
| 'getClockWriter'
|
|
28
30
|
| 'getComponentChange'
|
|
29
31
|
| 'getComponentMutationEpoch'
|
|
32
|
+
| 'getComponentMutationEpochs'
|
|
30
33
|
| 'getEntityArchetype'
|
|
31
34
|
| 'getFixedAccumulator'
|
|
32
35
|
| 'getFreeIndices'
|
|
@@ -41,28 +44,30 @@ type InternalName =
|
|
|
41
44
|
| 'getSchedules'
|
|
42
45
|
| 'getSharedRefs'
|
|
43
46
|
| 'getStructureEpoch'
|
|
47
|
+
| 'getStructuralEvidence'
|
|
44
48
|
| 'getUniqueRefs'
|
|
45
49
|
| 'lookupAlive'
|
|
46
50
|
| 'markComponentAdded'
|
|
47
51
|
| 'markComponentChanged'
|
|
48
52
|
| 'markComponentRangeChanged'
|
|
49
53
|
| 'markComponentsAdded'
|
|
50
|
-
| 'markDerivedComponentChanges'
|
|
51
54
|
| 'markStructureChanged'
|
|
52
55
|
| 'materializePendingEntity'
|
|
53
56
|
| 'nextMutationEpoch'
|
|
54
57
|
| 'poisonExecution'
|
|
58
|
+
| 'publishDerivedRange'
|
|
59
|
+
| 'prepareRelationshipInsert'
|
|
55
60
|
| 'preflightComponentData'
|
|
56
|
-
| 'readChangesSince'
|
|
57
61
|
| 'readRow'
|
|
62
|
+
| 'recordStructuralEvidence'
|
|
58
63
|
| 'recordIsLive'
|
|
59
64
|
| 'relationshipOnInsert'
|
|
60
65
|
| 'relationshipOnRemove'
|
|
61
66
|
| 'releaseManagedRefsOnRow'
|
|
62
|
-
| '
|
|
67
|
+
| 'releaseRelationshipPreparation'
|
|
63
68
|
| 'removeComponentCore'
|
|
64
|
-
| 'removeEntityChanges'
|
|
65
69
|
| 'routeError'
|
|
70
|
+
| 'restoreMutationEpoch'
|
|
66
71
|
| 'setFixedAccumulator'
|
|
67
72
|
| 'setQueryRow'
|
|
68
73
|
| 'spawnCore'
|