@forgeax/engine-ecs 0.1.21 → 0.1.23
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 +50 -16
- 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 +7 -1
- 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 +415 -128
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +15 -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 +48 -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 +4 -3
- package/dist/world-component-access.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 +10 -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__/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 +12 -2
- package/src/errors/query-and-component-errors.ts +1 -1
- package/src/errors/validation-errors.ts +30 -0
- package/src/errors.ts +29 -5
- package/src/internal.ts +56 -0
- package/src/projection/index.ts +68 -149
- package/src/query/derived-range-writer.ts +332 -0
- package/src/query/query.ts +89 -15
- 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 +53 -5
- package/src/world-core.ts +2 -2
- package/src/world-entity-lifecycle.ts +10 -1
- package/src/world-internal.ts +5 -5
- package/src/world.ts +45 -50
- 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
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import type { Component } from '../component';
|
|
3
|
+
import * as componentOwner from '../component';
|
|
4
|
+
import {
|
|
5
|
+
DerivedRangeOutOfBoundsError,
|
|
6
|
+
SharedKernelFailureError,
|
|
7
|
+
WorldPoisonedError,
|
|
8
|
+
} from '../errors';
|
|
9
|
+
import type { Table } from '../storage/table';
|
|
10
|
+
import type { EcsError, World } from '../world';
|
|
11
|
+
import { worldInternal } from '../world-internal';
|
|
12
|
+
import type { MutableColumnShape, ReadonlyColumnShape } from './query';
|
|
13
|
+
|
|
14
|
+
/** Package-internal whole-column storage owned by one dense query table. */
|
|
15
|
+
export interface DerivedColumnBinding<R extends Component, C extends Component> {
|
|
16
|
+
readonly read: ReadonlyColumnShape<R>;
|
|
17
|
+
readonly write: MutableColumnShape<C>;
|
|
18
|
+
readonly rowCapacity: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DerivedRangeKernel<R extends Component, C extends Component> = (
|
|
22
|
+
binding: DerivedColumnBinding<R, C>,
|
|
23
|
+
base: number,
|
|
24
|
+
start: number,
|
|
25
|
+
count: number,
|
|
26
|
+
context: unknown,
|
|
27
|
+
) => void;
|
|
28
|
+
|
|
29
|
+
/** Package-internal row probe used when the probe already has the derived value in scratch storage. */
|
|
30
|
+
export type DerivedRangeRowProbe<R extends Component, C extends Component> = (
|
|
31
|
+
binding: DerivedColumnBinding<R, C>,
|
|
32
|
+
row: number,
|
|
33
|
+
context: unknown,
|
|
34
|
+
) => boolean;
|
|
35
|
+
|
|
36
|
+
/** Package-internal commit for a value prepared by a preceding row probe. */
|
|
37
|
+
export type DerivedRangeRowCommit<R extends Component, C extends Component> = (
|
|
38
|
+
binding: DerivedColumnBinding<R, C>,
|
|
39
|
+
row: number,
|
|
40
|
+
context: unknown,
|
|
41
|
+
) => void;
|
|
42
|
+
|
|
43
|
+
interface DerivedRangeBindingSource {
|
|
44
|
+
readonly structureEpoch: () => number;
|
|
45
|
+
readonly tables: () => readonly Table[];
|
|
46
|
+
readonly readComponents: readonly Component[];
|
|
47
|
+
readonly writeComponents: readonly Component[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DerivedRangeAllocationTrace {
|
|
51
|
+
subarrayCalls: number;
|
|
52
|
+
shapeAllocations: number;
|
|
53
|
+
closureAllocations: number;
|
|
54
|
+
columnWindowAllocations: number;
|
|
55
|
+
rowFacadeAllocations: number;
|
|
56
|
+
proxyAllocations: number;
|
|
57
|
+
rangeCacheAllocations: number;
|
|
58
|
+
bindingRebuilds: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let allocationTrace: DerivedRangeAllocationTrace | undefined;
|
|
62
|
+
|
|
63
|
+
export function beginDerivedRangeAllocationTrace(): void {
|
|
64
|
+
allocationTrace = {
|
|
65
|
+
subarrayCalls: 0,
|
|
66
|
+
shapeAllocations: 0,
|
|
67
|
+
closureAllocations: 0,
|
|
68
|
+
columnWindowAllocations: 0,
|
|
69
|
+
rowFacadeAllocations: 0,
|
|
70
|
+
proxyAllocations: 0,
|
|
71
|
+
rangeCacheAllocations: 0,
|
|
72
|
+
bindingRebuilds: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function endDerivedRangeAllocationTrace(): DerivedRangeAllocationTrace {
|
|
77
|
+
const trace = allocationTrace;
|
|
78
|
+
allocationTrace = undefined;
|
|
79
|
+
return (
|
|
80
|
+
trace ?? {
|
|
81
|
+
subarrayCalls: 0,
|
|
82
|
+
shapeAllocations: 0,
|
|
83
|
+
closureAllocations: 0,
|
|
84
|
+
columnWindowAllocations: 0,
|
|
85
|
+
rowFacadeAllocations: 0,
|
|
86
|
+
proxyAllocations: 0,
|
|
87
|
+
rangeCacheAllocations: 0,
|
|
88
|
+
bindingRebuilds: 0,
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function countAllocation(name: keyof DerivedRangeAllocationTrace): void {
|
|
94
|
+
const trace = allocationTrace;
|
|
95
|
+
if (trace !== undefined) trace[name] += 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DerivedRangeWriter<R extends Component, C extends Component> {
|
|
99
|
+
readonly bindings: readonly DerivedColumnBinding<R, C>[];
|
|
100
|
+
writeRange(
|
|
101
|
+
bindingIndex: number,
|
|
102
|
+
base: number,
|
|
103
|
+
start: number,
|
|
104
|
+
count: number,
|
|
105
|
+
kernel: DerivedRangeKernel<R, C>,
|
|
106
|
+
context: unknown,
|
|
107
|
+
): Result<void, EcsError>;
|
|
108
|
+
/**
|
|
109
|
+
* Probe and commit rows in one pass. The probe must only inspect the derived
|
|
110
|
+
* value prepared in reusable scratch storage; the commit copies that value.
|
|
111
|
+
* The writer publishes only contiguous changed runs and does not allocate in
|
|
112
|
+
* the hot loop.
|
|
113
|
+
*/
|
|
114
|
+
probeAndCommitRange(
|
|
115
|
+
bindingIndex: number,
|
|
116
|
+
base: number,
|
|
117
|
+
start: number,
|
|
118
|
+
count: number,
|
|
119
|
+
probe: DerivedRangeRowProbe<R, C>,
|
|
120
|
+
commit: DerivedRangeRowCommit<R, C>,
|
|
121
|
+
context: unknown,
|
|
122
|
+
): Result<void, EcsError>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createDerivedRangeWriter<R extends Component, C extends Component>(
|
|
126
|
+
world: World,
|
|
127
|
+
component: C,
|
|
128
|
+
source: DerivedRangeBindingSource,
|
|
129
|
+
): DerivedRangeWriter<R, C> {
|
|
130
|
+
let boundEpoch = -1;
|
|
131
|
+
let bindingTables: readonly Table[] = [];
|
|
132
|
+
let bindings: readonly DerivedColumnBinding<R, C>[] = [];
|
|
133
|
+
let runStartBuffers: readonly Int32Array[] = [];
|
|
134
|
+
let runCountBuffers: readonly Int32Array[] = [];
|
|
135
|
+
|
|
136
|
+
const rebind = (): void => {
|
|
137
|
+
countAllocation('bindingRebuilds');
|
|
138
|
+
const tables = source.tables();
|
|
139
|
+
const nextBindings: DerivedColumnBinding<R, C>[] = [];
|
|
140
|
+
const nextRunStartBuffers: Int32Array[] = [];
|
|
141
|
+
const nextRunCountBuffers: Int32Array[] = [];
|
|
142
|
+
for (const table of tables) {
|
|
143
|
+
nextBindings.push({
|
|
144
|
+
read: buildWholeColumnShape(table, source.readComponents) as ReadonlyColumnShape<R>,
|
|
145
|
+
write: buildWholeColumnShape(table, source.writeComponents) as MutableColumnShape<C>,
|
|
146
|
+
rowCapacity: table.size,
|
|
147
|
+
});
|
|
148
|
+
// A table with N rows can contain at most N changed runs. These buffers
|
|
149
|
+
// are owned by the structural rebind and reused by every hot call.
|
|
150
|
+
const runCapacity = Math.max(1, table.size);
|
|
151
|
+
nextRunStartBuffers.push(new Int32Array(runCapacity));
|
|
152
|
+
nextRunCountBuffers.push(new Int32Array(runCapacity));
|
|
153
|
+
}
|
|
154
|
+
bindingTables = tables;
|
|
155
|
+
bindings = nextBindings;
|
|
156
|
+
runStartBuffers = nextRunStartBuffers;
|
|
157
|
+
runCountBuffers = nextRunCountBuffers;
|
|
158
|
+
boundEpoch = source.structureEpoch();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
rebind();
|
|
162
|
+
|
|
163
|
+
const writer: DerivedRangeWriter<R, C> = {
|
|
164
|
+
get bindings() {
|
|
165
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
166
|
+
return bindings;
|
|
167
|
+
},
|
|
168
|
+
writeRange(bindingIndex, base, start, count, kernel, context) {
|
|
169
|
+
if (world.execution.health === 'poisoned') {
|
|
170
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
171
|
+
}
|
|
172
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
173
|
+
const binding = bindings[bindingIndex];
|
|
174
|
+
const table = bindingTables[bindingIndex];
|
|
175
|
+
if (
|
|
176
|
+
binding === undefined ||
|
|
177
|
+
table === undefined ||
|
|
178
|
+
!Number.isSafeInteger(bindingIndex) ||
|
|
179
|
+
!Number.isSafeInteger(base) ||
|
|
180
|
+
!Number.isSafeInteger(start) ||
|
|
181
|
+
!Number.isSafeInteger(count) ||
|
|
182
|
+
bindingIndex < 0 ||
|
|
183
|
+
base < 0 ||
|
|
184
|
+
start < 0 ||
|
|
185
|
+
count < 0 ||
|
|
186
|
+
base + start + count > binding.rowCapacity ||
|
|
187
|
+
table.storage.get(componentOwner.componentId(component)) === undefined
|
|
188
|
+
) {
|
|
189
|
+
return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
|
|
190
|
+
}
|
|
191
|
+
if (count === 0) return ok(undefined);
|
|
192
|
+
|
|
193
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
194
|
+
let epoch: number;
|
|
195
|
+
try {
|
|
196
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
197
|
+
} catch (cause) {
|
|
198
|
+
return err(cause as EcsError);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
kernel(binding, base, start, count, context);
|
|
202
|
+
world[worldInternal].publishDerivedRange(
|
|
203
|
+
table,
|
|
204
|
+
componentOwner.componentId(component),
|
|
205
|
+
base + start,
|
|
206
|
+
count,
|
|
207
|
+
epoch,
|
|
208
|
+
);
|
|
209
|
+
return ok(undefined);
|
|
210
|
+
} catch (cause) {
|
|
211
|
+
world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
212
|
+
world[worldInternal].poisonExecution({
|
|
213
|
+
code: 'shared-kernel-failed',
|
|
214
|
+
kernelName: `derived-range:${component.name}`,
|
|
215
|
+
cause,
|
|
216
|
+
partialWrite: true,
|
|
217
|
+
retryable: false,
|
|
218
|
+
});
|
|
219
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
probeAndCommitRange(bindingIndex, base, start, count, probe, commit, context) {
|
|
223
|
+
if (world.execution.health === 'poisoned') {
|
|
224
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
225
|
+
}
|
|
226
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
227
|
+
const binding = bindings[bindingIndex];
|
|
228
|
+
const table = bindingTables[bindingIndex];
|
|
229
|
+
const runStarts = runStartBuffers[bindingIndex];
|
|
230
|
+
const runCounts = runCountBuffers[bindingIndex];
|
|
231
|
+
if (
|
|
232
|
+
binding === undefined ||
|
|
233
|
+
table === undefined ||
|
|
234
|
+
runStarts === undefined ||
|
|
235
|
+
runCounts === undefined ||
|
|
236
|
+
!Number.isSafeInteger(bindingIndex) ||
|
|
237
|
+
!Number.isSafeInteger(base) ||
|
|
238
|
+
!Number.isSafeInteger(start) ||
|
|
239
|
+
!Number.isSafeInteger(count) ||
|
|
240
|
+
bindingIndex < 0 ||
|
|
241
|
+
base < 0 ||
|
|
242
|
+
start < 0 ||
|
|
243
|
+
count < 0 ||
|
|
244
|
+
base + start + count > binding.rowCapacity ||
|
|
245
|
+
table.storage.get(componentOwner.componentId(component)) === undefined
|
|
246
|
+
) {
|
|
247
|
+
return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
|
|
248
|
+
}
|
|
249
|
+
if (count === 0) return ok(undefined);
|
|
250
|
+
|
|
251
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
252
|
+
let runCount = 0;
|
|
253
|
+
let epoch = 0;
|
|
254
|
+
let epochReserved = false;
|
|
255
|
+
let runStart = -1;
|
|
256
|
+
try {
|
|
257
|
+
for (let offset = 0; offset < count; offset += 1) {
|
|
258
|
+
const row = base + start + offset;
|
|
259
|
+
const changed = probe(binding, row, context);
|
|
260
|
+
if (changed) {
|
|
261
|
+
if (!epochReserved) {
|
|
262
|
+
try {
|
|
263
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
264
|
+
epochReserved = true;
|
|
265
|
+
} catch (cause) {
|
|
266
|
+
// Nothing has been committed yet. Keep the World healthy and
|
|
267
|
+
// preserve the source error so the caller can retry unchanged.
|
|
268
|
+
return err(cause as EcsError);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (runStart < 0) runStart = row;
|
|
272
|
+
commit(binding, row, context);
|
|
273
|
+
} else if (runStart >= 0) {
|
|
274
|
+
runStarts[runCount] = runStart;
|
|
275
|
+
runCounts[runCount] = row - runStart;
|
|
276
|
+
runCount += 1;
|
|
277
|
+
runStart = -1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (runStart >= 0) {
|
|
281
|
+
runStarts[runCount] = runStart;
|
|
282
|
+
runCounts[runCount] = base + start + count - runStart;
|
|
283
|
+
runCount += 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Every changed run in this call shares the epoch reserved immediately
|
|
287
|
+
// before the first commit. Publication remains deferred until the
|
|
288
|
+
// complete probe/commit pass has succeeded.
|
|
289
|
+
if (runCount > 0) {
|
|
290
|
+
const componentIdentifier = componentOwner.componentId(component);
|
|
291
|
+
for (let index = 0; index < runCount; index += 1) {
|
|
292
|
+
const runStartValue = runStarts[index] ?? 0;
|
|
293
|
+
const runLength = runCounts[index] ?? 0;
|
|
294
|
+
world[worldInternal].publishDerivedRange(
|
|
295
|
+
table,
|
|
296
|
+
componentIdentifier,
|
|
297
|
+
runStartValue,
|
|
298
|
+
runLength,
|
|
299
|
+
epoch,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return ok(undefined);
|
|
304
|
+
} catch (cause) {
|
|
305
|
+
if (epochReserved) world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
306
|
+
world[worldInternal].poisonExecution({
|
|
307
|
+
code: 'shared-kernel-failed',
|
|
308
|
+
kernelName: `derived-range:${component.name}:probe`,
|
|
309
|
+
cause,
|
|
310
|
+
partialWrite: true,
|
|
311
|
+
retryable: false,
|
|
312
|
+
});
|
|
313
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
return writer;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildWholeColumnShape(
|
|
321
|
+
table: Table,
|
|
322
|
+
components: readonly Component[],
|
|
323
|
+
): Record<string, unknown> {
|
|
324
|
+
countAllocation('shapeAllocations');
|
|
325
|
+
const shape: Record<string, unknown> = {};
|
|
326
|
+
for (const component of components) {
|
|
327
|
+
const fields = table.storage.get(componentOwner.componentId(component))?.fields;
|
|
328
|
+
if (fields === undefined) continue;
|
|
329
|
+
for (const [fieldName, column] of fields) shape[fieldName] = column.view;
|
|
330
|
+
}
|
|
331
|
+
return shape;
|
|
332
|
+
}
|
package/src/query/query.ts
CHANGED
|
@@ -19,12 +19,14 @@ import {
|
|
|
19
19
|
QuerySpanUnavailableError,
|
|
20
20
|
type QuerySpanUnavailableReason,
|
|
21
21
|
} from '../errors';
|
|
22
|
+
import { DERIVED_WRITER } from '../internal';
|
|
22
23
|
import type { Archetype, ArchetypeId } from '../storage/archetype';
|
|
23
24
|
import type { ArchetypeGraph } from '../storage/archetype-graph';
|
|
24
25
|
import { sparseTagIndex } from '../storage/change-detection';
|
|
25
26
|
import type { FieldView, ManagedColumnReader } from '../storage/column';
|
|
26
27
|
import type { Table } from '../storage/table';
|
|
27
28
|
import { type WorldInternal, worldInternal } from '../world-internal';
|
|
29
|
+
import { createDerivedRangeWriter, type DerivedRangeWriter } from './derived-range-writer';
|
|
28
30
|
|
|
29
31
|
export interface QueryDescriptor<
|
|
30
32
|
R extends readonly Component[] = readonly Component[],
|
|
@@ -91,7 +93,7 @@ export interface Query<
|
|
|
91
93
|
* Read one known entity without iterating the query's matching tables.
|
|
92
94
|
* Structural descriptor constraints are applied; temporal `changed` and
|
|
93
95
|
* `added` filters are intentionally not evaluated because this is an
|
|
94
|
-
* identity-addressed read, not a
|
|
96
|
+
* identity-addressed read, not a version scan.
|
|
95
97
|
*/
|
|
96
98
|
at(entity: EntityHandle): QueryRow<R, W, O> | undefined;
|
|
97
99
|
spans(): Result<Iterable<QuerySpan<R, W>>, QuerySpanUnavailableError>;
|
|
@@ -254,8 +256,13 @@ class QuerySpanFacade<R extends readonly Component[], W extends readonly Compone
|
|
|
254
256
|
readonly length: number,
|
|
255
257
|
) {
|
|
256
258
|
const entityColumn = table.storage.get(componentId(Entity))?.fields.get('self');
|
|
257
|
-
this.entities = (
|
|
258
|
-
|
|
259
|
+
this.entities = (
|
|
260
|
+
entityColumn === undefined
|
|
261
|
+
? new Uint32Array(0)
|
|
262
|
+
: rowStart === 0 && length === entityColumn.view.length
|
|
263
|
+
? entityColumn.view
|
|
264
|
+
: entityColumn.view.subarray(rowStart, rowStart + length)
|
|
265
|
+
) as Readonly<Uint32Array>;
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
get<C extends R[number]>(component: C): ReadonlyColumnShape<C> {
|
|
@@ -288,7 +295,7 @@ function makeManagedColumnReader(
|
|
|
288
295
|
length: number,
|
|
289
296
|
fieldType: string,
|
|
290
297
|
): ManagedColumnReader<string> {
|
|
291
|
-
const slots = view.subarray(0, length);
|
|
298
|
+
const slots = view.length === length ? view : view.subarray(0, length);
|
|
292
299
|
return Object.freeze({
|
|
293
300
|
length,
|
|
294
301
|
get(i: number): number {
|
|
@@ -310,7 +317,8 @@ function buildColumnShape(
|
|
|
310
317
|
for (const [fieldName, column] of fields) {
|
|
311
318
|
const start = rowStart * column.arity;
|
|
312
319
|
const end = start + rowCount * column.arity;
|
|
313
|
-
const view =
|
|
320
|
+
const view =
|
|
321
|
+
start === 0 && end === column.view.length ? column.view : column.view.subarray(start, end);
|
|
314
322
|
const fieldType = componentSchema(component)[fieldName];
|
|
315
323
|
shape[fieldName] =
|
|
316
324
|
fieldType !== undefined && isManagedField(fieldType)
|
|
@@ -328,6 +336,7 @@ class ExecutableQuery<
|
|
|
328
336
|
{
|
|
329
337
|
private matchedArchetypes: ArchetypeId[] = [];
|
|
330
338
|
private matchedTables: number[] = [];
|
|
339
|
+
private matchedTableObjects: Table[] = [];
|
|
331
340
|
private lastGraphGeneration = -1;
|
|
332
341
|
private lastObservedEpoch = 0;
|
|
333
342
|
private active = false;
|
|
@@ -432,7 +441,8 @@ class ExecutableQuery<
|
|
|
432
441
|
query.refreshMatches();
|
|
433
442
|
const structureEpoch = query.world[worldInternal].getStructureEpoch();
|
|
434
443
|
const upperBound = query.world[worldInternal].getMutationEpoch();
|
|
435
|
-
let
|
|
444
|
+
let tableIndex = 0;
|
|
445
|
+
let rowIndex = 0;
|
|
436
446
|
let finished = false;
|
|
437
447
|
const close = (commit: boolean): void => {
|
|
438
448
|
if (finished) return;
|
|
@@ -450,13 +460,44 @@ class ExecutableQuery<
|
|
|
450
460
|
query.world[worldInternal].getStructureEpoch(),
|
|
451
461
|
);
|
|
452
462
|
}
|
|
453
|
-
while (
|
|
463
|
+
while (tableIndex < query.matchedTables.length) {
|
|
454
464
|
const table =
|
|
455
|
-
query.world[worldInternal].getGraph().tables[query.matchedTables[
|
|
456
|
-
if (table === undefined || table.size === 0)
|
|
465
|
+
query.world[worldInternal].getGraph().tables[query.matchedTables[tableIndex] ?? -1];
|
|
466
|
+
if (table === undefined || table.size === 0) {
|
|
467
|
+
tableIndex += 1;
|
|
468
|
+
rowIndex = 0;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (query.compiled.changedIds.length === 0 && query.compiled.addedIds.length === 0) {
|
|
472
|
+
tableIndex += 1;
|
|
473
|
+
rowIndex = 0;
|
|
474
|
+
return {
|
|
475
|
+
done: false,
|
|
476
|
+
value: new QuerySpanFacade<R, W>(query.world, table, 0, table.size),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
while (
|
|
480
|
+
rowIndex < table.size &&
|
|
481
|
+
!query.denseChangeMatches(table, rowIndex, upperBound)
|
|
482
|
+
) {
|
|
483
|
+
rowIndex += 1;
|
|
484
|
+
}
|
|
485
|
+
if (rowIndex >= table.size) {
|
|
486
|
+
tableIndex += 1;
|
|
487
|
+
rowIndex = 0;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const start = rowIndex;
|
|
491
|
+
rowIndex += 1;
|
|
492
|
+
while (
|
|
493
|
+
rowIndex < table.size &&
|
|
494
|
+
query.denseChangeMatches(table, rowIndex, upperBound)
|
|
495
|
+
) {
|
|
496
|
+
rowIndex += 1;
|
|
497
|
+
}
|
|
457
498
|
return {
|
|
458
499
|
done: false,
|
|
459
|
-
value: new QuerySpanFacade<R, W>(query.world, table,
|
|
500
|
+
value: new QuerySpanFacade<R, W>(query.world, table, start, rowIndex - start),
|
|
460
501
|
};
|
|
461
502
|
}
|
|
462
503
|
close(true);
|
|
@@ -471,6 +512,28 @@ class ExecutableQuery<
|
|
|
471
512
|
});
|
|
472
513
|
}
|
|
473
514
|
|
|
515
|
+
[DERIVED_WRITER]<C extends W[number]>(
|
|
516
|
+
component: C,
|
|
517
|
+
): Result<DerivedRangeWriter<R[number], C>, QuerySpanUnavailableError> {
|
|
518
|
+
const reason = this.spanUnavailableReason();
|
|
519
|
+
if (reason !== undefined) return err(new QuerySpanUnavailableError(reason));
|
|
520
|
+
return ok(
|
|
521
|
+
createDerivedRangeWriter<R[number], C>(
|
|
522
|
+
this.world as unknown as import('../world').World,
|
|
523
|
+
component,
|
|
524
|
+
{
|
|
525
|
+
structureEpoch: () => this.world[worldInternal].getStructureEpoch(),
|
|
526
|
+
tables: () => {
|
|
527
|
+
this.refreshMatches();
|
|
528
|
+
return this.matchedTableObjects;
|
|
529
|
+
},
|
|
530
|
+
readComponents: this.compiled.descriptor.read ?? [],
|
|
531
|
+
writeComponents: this.compiled.descriptor.write ?? [],
|
|
532
|
+
},
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
474
537
|
combinations(k = 2): Iterable<readonly QueryRow<R, W, O>[]> {
|
|
475
538
|
const query = this;
|
|
476
539
|
return {
|
|
@@ -527,6 +590,10 @@ class ExecutableQuery<
|
|
|
527
590
|
this.matchedTables = graph.tables
|
|
528
591
|
.filter((table) => this.tableMatches(table))
|
|
529
592
|
.map((table) => table.id);
|
|
593
|
+
this.matchedTableObjects = this.matchedTables.flatMap((id) => {
|
|
594
|
+
const table = graph.tables[id];
|
|
595
|
+
return table === undefined ? [] : [table];
|
|
596
|
+
});
|
|
530
597
|
this.lastGraphGeneration = graph.generation;
|
|
531
598
|
}
|
|
532
599
|
|
|
@@ -580,14 +647,21 @@ class ExecutableQuery<
|
|
|
580
647
|
return true;
|
|
581
648
|
}
|
|
582
649
|
|
|
650
|
+
private denseChangeMatches(table: Table, row: number, upperBound: number): boolean {
|
|
651
|
+
for (const componentId of this.compiled.changedIds) {
|
|
652
|
+
const epoch = table.storage.get(componentId)?.epochs.changed[row] ?? 0;
|
|
653
|
+
if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
|
|
654
|
+
}
|
|
655
|
+
for (const componentId of this.compiled.addedIds) {
|
|
656
|
+
const epoch = table.storage.get(componentId)?.epochs.added[row] ?? 0;
|
|
657
|
+
if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
|
|
583
662
|
private spanUnavailableReason(): QuerySpanUnavailableReason | undefined {
|
|
584
663
|
if (this.compiled.sparseRoute) return 'sparse-component';
|
|
585
664
|
if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return 'optional-data';
|
|
586
|
-
if (
|
|
587
|
-
(this.compiled.descriptor.changed?.length ?? 0) > 0 ||
|
|
588
|
-
(this.compiled.descriptor.added?.length ?? 0) > 0
|
|
589
|
-
)
|
|
590
|
-
return 'row-change-filter';
|
|
591
665
|
return undefined;
|
|
592
666
|
}
|
|
593
667
|
}
|
|
@@ -24,6 +24,14 @@ export interface RelationshipOptions {
|
|
|
24
24
|
readonly sourceField: string;
|
|
25
25
|
readonly targetName: string;
|
|
26
26
|
readonly targetField: string;
|
|
27
|
+
/**
|
|
28
|
+
* Components materialized with the writable source side. This is the
|
|
29
|
+
* relationship equivalent of `defineComponent(..., { requires })`: the
|
|
30
|
+
* dependency is resolved once at the structural boundary, never by a frame
|
|
31
|
+
* system. It applies only to the source; the reverse target remains an
|
|
32
|
+
* engine-owned projection.
|
|
33
|
+
*/
|
|
34
|
+
readonly sourceRequires?: readonly Component[];
|
|
27
35
|
readonly exclusive?: boolean;
|
|
28
36
|
readonly linkedSpawn?: boolean;
|
|
29
37
|
/** Allow a source to point at its own holder (e.g. self-animated entities). */
|
|
@@ -75,7 +83,11 @@ export function defineRelationship<
|
|
|
75
83
|
};
|
|
76
84
|
const target = defineComponent(options.targetName, targetFields);
|
|
77
85
|
const sourceFields: FieldsInput = { [options.sourceField]: { type: 'entity' } };
|
|
78
|
-
const source = defineComponent(
|
|
86
|
+
const source = defineComponent(
|
|
87
|
+
options.sourceName,
|
|
88
|
+
sourceFields,
|
|
89
|
+
options.sourceRequires === undefined ? undefined : { requires: options.sourceRequires },
|
|
90
|
+
);
|
|
79
91
|
|
|
80
92
|
roles.set(source, {
|
|
81
93
|
kind: 'source',
|
package/src/shared-ref-store.ts
CHANGED
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
SharedRefStaleError,
|
|
85
85
|
} from './errors';
|
|
86
86
|
|
|
87
|
-
const
|
|
87
|
+
const SHARED_REF_RELEASE_EVIDENCE_CAPACITY = 4096;
|
|
88
88
|
|
|
89
89
|
export interface SharedRefMutation {
|
|
90
90
|
readonly epoch: number;
|
|
@@ -98,17 +98,10 @@ export interface SharedRefReleaseEvidence {
|
|
|
98
98
|
readonly evidence: 'released';
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
readonly records: readonly SharedRefMutation[];
|
|
106
|
-
}
|
|
107
|
-
| {
|
|
108
|
-
readonly status: 'overflow';
|
|
109
|
-
readonly cursor: number;
|
|
110
|
-
readonly oldestAvailable: number;
|
|
111
|
-
};
|
|
101
|
+
export interface SharedRefMutationRead {
|
|
102
|
+
readonly cursor: number;
|
|
103
|
+
readonly records: readonly SharedRefMutation[];
|
|
104
|
+
}
|
|
112
105
|
|
|
113
106
|
// MAX_SLOT is now imported from @forgeax/engine-types (codec SSOT, D-1).
|
|
114
107
|
// The local constant is removed to avoid drift (AC-15).
|
|
@@ -147,7 +140,8 @@ export class SharedRefStore {
|
|
|
147
140
|
>();
|
|
148
141
|
private nextSlot = BUILTIN_BASE;
|
|
149
142
|
private mutationEpoch = 0;
|
|
150
|
-
|
|
143
|
+
/** Latest published mutation epoch per live handle; not an event journal. */
|
|
144
|
+
private readonly mutationEpochs = new Map<number, number>();
|
|
151
145
|
private readonly releaseJournal: SharedRefReleaseEvidence[] = [];
|
|
152
146
|
|
|
153
147
|
/**
|
|
@@ -271,10 +265,7 @@ export class SharedRefStore {
|
|
|
271
265
|
throw new RangeError('SharedRefStore mutation epoch exhausted');
|
|
272
266
|
}
|
|
273
267
|
this.mutationEpoch += 1;
|
|
274
|
-
this.
|
|
275
|
-
if (this.mutationJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
|
|
276
|
-
this.mutationJournal.shift();
|
|
277
|
-
}
|
|
268
|
+
this.mutationEpochs.set(unwrapHandle(handle), this.mutationEpoch);
|
|
278
269
|
return ok(undefined);
|
|
279
270
|
}
|
|
280
271
|
|
|
@@ -283,16 +274,16 @@ export class SharedRefStore {
|
|
|
283
274
|
return this.mutationEpoch;
|
|
284
275
|
}
|
|
285
276
|
|
|
286
|
-
/** Read
|
|
277
|
+
/** Read each live handle whose latest published mutation is after `cursor`. */
|
|
287
278
|
readChangesSince(cursor: number): SharedRefMutationRead {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
279
|
+
const records: SharedRefMutation[] = [];
|
|
280
|
+
for (const [handle, epoch] of this.mutationEpochs) {
|
|
281
|
+
if (epoch > cursor) records.push({ epoch, handle });
|
|
291
282
|
}
|
|
283
|
+
records.sort((left, right) => left.epoch - right.epoch || left.handle - right.handle);
|
|
292
284
|
return {
|
|
293
|
-
status: 'ok',
|
|
294
285
|
cursor: this.mutationEpoch,
|
|
295
|
-
records
|
|
286
|
+
records,
|
|
296
287
|
};
|
|
297
288
|
}
|
|
298
289
|
|
|
@@ -372,6 +363,7 @@ export class SharedRefStore {
|
|
|
372
363
|
}
|
|
373
364
|
this.refcounts.delete(raw);
|
|
374
365
|
this.payloads.delete(raw);
|
|
366
|
+
this.mutationEpochs.delete(raw);
|
|
375
367
|
// Gen increment + retire (AC-07): bump gen; once it would exceed MAX_GEN
|
|
376
368
|
// (gen 255 is still usable; the bump to 256 triggers retire) the slot is
|
|
377
369
|
// permanently retired — NOT pushed to freeSlots. This prevents handle
|
|
@@ -389,7 +381,7 @@ export class SharedRefStore {
|
|
|
389
381
|
evidence: 'released' as const,
|
|
390
382
|
});
|
|
391
383
|
this.releaseJournal.push(evidence);
|
|
392
|
-
if (this.releaseJournal.length >
|
|
384
|
+
if (this.releaseJournal.length > SHARED_REF_RELEASE_EVIDENCE_CAPACITY) {
|
|
393
385
|
this.releaseJournal.shift();
|
|
394
386
|
}
|
|
395
387
|
return ok(evidence);
|
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Component } from '../component';
|
|
4
4
|
import * as componentOwner from '../component';
|
|
5
|
-
import { Entity } from '../entity';
|
|
6
5
|
import { type EntityHandle, entityIndex } from '../entity-handle';
|
|
7
6
|
|
|
8
|
-
export { WorldChangeJournal, type WorldChangeRead } from '../world-change-journal';
|
|
9
|
-
|
|
10
7
|
import type { ArchetypeGraph } from './archetype-graph';
|
|
11
8
|
import { getOrCreateSparseTagSet } from './archetype-graph';
|
|
12
|
-
import type { Table } from './table';
|
|
13
9
|
|
|
14
10
|
const INITIAL_SPARSE_CAPACITY = 64;
|
|
15
11
|
|
|
@@ -218,14 +214,3 @@ export function markComponentChanged(
|
|
|
218
214
|
const tableRow = archetype.rows[location.archetypeRow] ?? -1;
|
|
219
215
|
epochs.changed[tableRow] = epoch();
|
|
220
216
|
}
|
|
221
|
-
|
|
222
|
-
export function readTableEntityRange(
|
|
223
|
-
table: Table,
|
|
224
|
-
rowStart: number,
|
|
225
|
-
rowCount: number,
|
|
226
|
-
): readonly EntityHandle[] {
|
|
227
|
-
const entities = table.storage.get(componentOwner.componentId(Entity))?.fields.get('self')
|
|
228
|
-
?.view as Uint32Array | undefined;
|
|
229
|
-
if (entities === undefined) return [];
|
|
230
|
-
return entities.subarray(rowStart, rowStart + rowCount) as unknown as readonly EntityHandle[];
|
|
231
|
-
}
|