@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.
Files changed (104) hide show
  1. package/README.md +83 -17
  2. package/dist/__tests__/changed-block-summary.candidate.test.d.ts +2 -0
  3. package/dist/__tests__/changed-block-summary.candidate.test.d.ts.map +1 -0
  4. package/dist/__tests__/component-requirements.unit.test.d.ts +2 -0
  5. package/dist/__tests__/component-requirements.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/component-version-surface.test.d.ts +2 -0
  7. package/dist/__tests__/component-version-surface.test.d.ts.map +1 -0
  8. package/dist/__tests__/derived-range-writer.contract.test-d.d.ts +2 -0
  9. package/dist/__tests__/derived-range-writer.contract.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/derived-range-writer.contract.test.d.ts +2 -0
  11. package/dist/__tests__/derived-range-writer.contract.test.d.ts.map +1 -0
  12. package/dist/__tests__/structural-evidence.contract.test-d.d.ts +2 -0
  13. package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/structural-evidence.contract.test.d.ts +2 -0
  15. package/dist/__tests__/structural-evidence.contract.test.d.ts.map +1 -0
  16. package/dist/component-schema.d.ts +17 -0
  17. package/dist/component-schema.d.ts.map +1 -1
  18. package/dist/component.d.ts +16 -7
  19. package/dist/component.d.ts.map +1 -1
  20. package/dist/errors/query-and-component-errors.d.ts +1 -1
  21. package/dist/errors/query-and-component-errors.d.ts.map +1 -1
  22. package/dist/errors/validation-errors.d.ts +24 -0
  23. package/dist/errors/validation-errors.d.ts.map +1 -1
  24. package/dist/errors.d.ts +14 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/externalization/index.mjs.map +1 -1
  27. package/dist/index.mjs +977 -255
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/internal.d.ts +18 -0
  30. package/dist/internal.d.ts.map +1 -0
  31. package/dist/internal.mjs +20 -2
  32. package/dist/internal.mjs.map +1 -1
  33. package/dist/projection/index.d.ts +27 -52
  34. package/dist/projection/index.d.ts.map +1 -1
  35. package/dist/projection/index.mjs +66 -76
  36. package/dist/projection/index.mjs.map +1 -1
  37. package/dist/query/derived-range-writer.d.ts +62 -0
  38. package/dist/query/derived-range-writer.d.ts.map +1 -0
  39. package/dist/query/query.d.ts +1 -1
  40. package/dist/query/query.d.ts.map +1 -1
  41. package/dist/relationship-index.d.ts +8 -0
  42. package/dist/relationship-index.d.ts.map +1 -1
  43. package/dist/shared-ref-store.d.ts +5 -9
  44. package/dist/shared-ref-store.d.ts.map +1 -1
  45. package/dist/shared.mjs.map +1 -1
  46. package/dist/storage/change-detection.d.ts +1 -3
  47. package/dist/storage/change-detection.d.ts.map +1 -1
  48. package/dist/storage/structural-evidence.d.ts +30 -0
  49. package/dist/storage/structural-evidence.d.ts.map +1 -0
  50. package/dist/world-component-access.d.ts +65 -19
  51. package/dist/world-component-access.d.ts.map +1 -1
  52. package/dist/world-component-storage.d.ts +26 -10
  53. package/dist/world-component-storage.d.ts.map +1 -1
  54. package/dist/world-core.d.ts +2 -2
  55. package/dist/world-core.d.ts.map +1 -1
  56. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  57. package/dist/world-internal.d.ts +1 -1
  58. package/dist/world-internal.d.ts.map +1 -1
  59. package/dist/world.d.ts +15 -14
  60. package/dist/world.d.ts.map +1 -1
  61. package/package.json +5 -5
  62. package/src/__tests__/changed-block-summary.candidate.test.ts +57 -0
  63. package/src/__tests__/component-requirements.unit.test.ts +76 -0
  64. package/src/__tests__/component-version-surface.test.ts +45 -0
  65. package/src/__tests__/derived-range-writer.contract.test-d.ts +35 -0
  66. package/src/__tests__/derived-range-writer.contract.test.ts +339 -0
  67. package/src/__tests__/errors.unit.test.ts +21 -5
  68. package/src/__tests__/externalization-render-read-lease.test-d.ts +1 -1
  69. package/src/__tests__/externalization-render-read-lease.unit.test.ts +15 -15
  70. package/src/__tests__/query-storage-trends.perf.test.ts +19 -0
  71. package/src/__tests__/query.unit.test.ts +10 -6
  72. package/src/__tests__/relationship-index.test.ts +147 -1
  73. package/src/__tests__/shared-ref-store.unit.test.ts +3 -8
  74. package/src/__tests__/structural-evidence.contract.test-d.ts +6 -0
  75. package/src/__tests__/structural-evidence.contract.test.ts +49 -0
  76. package/src/component-schema.ts +51 -0
  77. package/src/component.ts +21 -8
  78. package/src/errors/query-and-component-errors.ts +4 -1
  79. package/src/errors/validation-errors.ts +30 -0
  80. package/src/errors.ts +29 -5
  81. package/src/internal.ts +59 -0
  82. package/src/projection/index.ts +68 -149
  83. package/src/query/derived-range-writer.ts +443 -0
  84. package/src/query/query.ts +115 -16
  85. package/src/relationship-index.ts +13 -1
  86. package/src/shared-ref-store.ts +16 -24
  87. package/src/storage/change-detection.ts +0 -15
  88. package/src/storage/structural-evidence.ts +64 -0
  89. package/src/world-component-access.ts +479 -130
  90. package/src/world-component-storage.ts +104 -18
  91. package/src/world-core.ts +2 -2
  92. package/src/world-entity-lifecycle.ts +59 -10
  93. package/src/world-internal.ts +10 -5
  94. package/src/world.ts +97 -53
  95. package/dist/.tsbuildinfo +0 -1
  96. package/dist/__tests__/world-change-journal.unit.test.d.ts +0 -2
  97. package/dist/__tests__/world-change-journal.unit.test.d.ts.map +0 -1
  98. package/dist/__tests__/world-internal-identity.unit.test.d.ts +0 -2
  99. package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +0 -1
  100. package/dist/world-change-journal.d.ts +0 -34
  101. package/dist/world-change-journal.d.ts.map +0 -1
  102. package/src/__tests__/world-change-journal.unit.test.ts +0 -97
  103. package/src/__tests__/world-internal-identity.unit.test.ts +0 -17
  104. package/src/world-change-journal.ts +0 -84
@@ -3,7 +3,7 @@ import type { Component } from '../component';
3
3
  import { componentId } from '../component';
4
4
  import type { EntityHandle } from '../entity-handle';
5
5
  import type { SharedRefMutationRead } from '../shared-ref-store';
6
- import type { WorldChangeRead } from '../storage/change-detection';
6
+ import type { StructuralEvidenceRead } from '../storage/structural-evidence';
7
7
  import type { EcsError, World } from '../world';
8
8
  import { worldInternal } from '../world-internal';
9
9
 
@@ -24,41 +24,9 @@ export {
24
24
  StaleEntityError,
25
25
  } from '../errors';
26
26
 
27
- export type ProjectionChangeKind =
28
- | 'component-added'
29
- | 'component-changed'
30
- | 'component-removed'
31
- | 'derived-component-changed'
32
- | 'entity-removed';
33
-
34
- export interface ProjectionChange {
35
- readonly entity: EntityHandle;
36
- readonly kind: ProjectionChangeKind;
37
- /** Token identity is the owner-facing component discriminator. */
38
- readonly component?: Component;
39
- /** Numeric id remains journal evidence for ECS-internal consumers. */
40
- readonly componentId?: number;
41
- }
42
-
43
- export type ProjectionRead =
44
- | {
45
- readonly status: 'delta';
46
- readonly cursor: number;
47
- readonly changes: readonly ProjectionChange[];
48
- }
49
- | {
50
- readonly status: 'rebuild';
51
- readonly cursor: number;
52
- readonly reason: 'journal-overflow';
53
- };
54
-
55
- export interface WorldProjection {
56
- readonly cursor: number;
57
- poll(): ProjectionRead;
58
- }
59
-
60
- export interface WorldProjectionOptions {
61
- readonly components?: readonly Component[];
27
+ /** Read producer-owned typed structural facts without inferring from a World scan. */
28
+ export function readStructuralEvidence(world: World, cursor: number): StructuralEvidenceRead {
29
+ return world[worldInternal].getStructuralEvidence().readAfter(cursor) as StructuralEvidenceRead;
62
30
  }
63
31
 
64
32
  export interface RenderProjectionComponentRequest {
@@ -83,28 +51,34 @@ export interface RenderProjectionSpans {
83
51
 
84
52
  export interface RenderChangeBatchOk {
85
53
  readonly status: 'ok';
86
- readonly cursor: number;
87
- readonly world: Extract<WorldChangeRead, { readonly status: 'ok' }>;
88
- readonly sharedRefs: Extract<SharedRefMutationRead, { readonly status: 'ok' }>;
54
+ readonly version: RenderReadVersion;
55
+ readonly world: RenderWorldChanges;
56
+ readonly sharedRefs: SharedRefMutationRead;
89
57
  }
90
58
 
91
- export interface RenderChangeBatchOverflow {
92
- readonly status: 'overflow';
93
- readonly cursor: number;
59
+ export interface RenderChangeBatchRebuild {
60
+ readonly status: 'rebuild';
61
+ readonly version: RenderReadVersion;
94
62
  readonly resync: true;
95
- readonly oldestAvailable: number;
96
- readonly world: WorldChangeRead;
63
+ readonly reason: 'structure-changed';
64
+ readonly world: RenderWorldChanges;
97
65
  readonly sharedRefs: SharedRefMutationRead;
98
66
  }
99
67
 
100
- export type RenderChangeBatch = RenderChangeBatchOk | RenderChangeBatchOverflow;
68
+ export interface RenderWorldChanges {
69
+ readonly fromEpoch: number;
70
+ readonly toEpoch: number;
71
+ readonly changedComponentIds: readonly number[];
72
+ }
73
+
74
+ export type RenderChangeBatch = RenderChangeBatchOk | RenderChangeBatchRebuild;
101
75
 
102
76
  export interface RenderReadLease {
103
77
  readonly worldIdentity: string;
104
78
  readonly generation: number;
105
- readChanges(cursor: number): RenderChangeBatch;
79
+ readChanges(version: RenderReadVersion): RenderChangeBatch;
106
80
  querySpans(request: RenderProjectionRequest): RenderProjectionSpans;
107
- inspectCursor(): number;
81
+ captureVersion(): RenderReadVersion;
108
82
  dispose(): void;
109
83
  }
110
84
 
@@ -124,9 +98,10 @@ export function readRenderArrayView(
124
98
  | undefined;
125
99
  }
126
100
 
127
- interface ProjectionCursor {
128
- readonly world: number;
129
- readonly sharedRefs: number;
101
+ export interface RenderReadVersion {
102
+ readonly mutationEpoch: number;
103
+ readonly structureEpoch: number;
104
+ readonly sharedRefEpoch: number;
130
105
  }
131
106
 
132
107
  function readProjectionSpans(
@@ -167,80 +142,78 @@ function readProjectionSpans(
167
142
  export function createRenderReadLease(world: World, token: object = {}): RenderReadLease {
168
143
  void token;
169
144
  const sharedRefs = world[worldInternal].getSharedRefs();
170
- const generation = Math.max(1, world[worldInternal].getStructureEpoch());
171
145
  let disposed = false;
172
- let nextCursor = 1;
173
- const cursors = new Map<number, ProjectionCursor>([
174
- [
175
- 0,
176
- { world: world[worldInternal].getChangeCursor(), sharedRefs: sharedRefs.getMutationEpoch() },
177
- ],
178
- ]);
179
146
 
180
147
  const assertLive = (): void => {
181
148
  if (disposed) throw new Error('RenderReadLease is disposed.');
182
149
  };
183
150
 
184
- const captureCursor = (): number => {
185
- const cursor = nextCursor++;
186
- cursors.set(cursor, {
187
- world: world[worldInternal].getChangeCursor(),
188
- sharedRefs: sharedRefs.getMutationEpoch(),
189
- });
190
- if (cursors.size > 8) {
191
- const oldest = cursors.keys().next().value;
192
- if (typeof oldest === 'number' && oldest !== cursor) cursors.delete(oldest);
193
- }
194
- return cursor;
151
+ const captureVersion = (): RenderReadVersion => {
152
+ return {
153
+ mutationEpoch: world[worldInternal].getMutationEpoch(),
154
+ structureEpoch: world[worldInternal].getStructureEpoch(),
155
+ sharedRefEpoch: sharedRefs.getMutationEpoch(),
156
+ };
195
157
  };
196
158
 
197
159
  return {
198
160
  worldIdentity: world.identity,
199
- generation,
200
- inspectCursor(): number {
161
+ get generation(): number {
162
+ return Math.max(1, world[worldInternal].getStructureEpoch());
163
+ },
164
+ captureVersion(): RenderReadVersion {
201
165
  assertLive();
202
- return captureCursor();
166
+ return captureVersion();
203
167
  },
204
- readChanges(cursor: number): RenderChangeBatch {
168
+ readChanges(start: RenderReadVersion): RenderChangeBatch {
205
169
  assertLive();
206
- const start = cursors.get(cursor);
207
- if (start === undefined) throw new RangeError(`Unknown RenderReadLease cursor ${cursor}.`);
208
- const worldRead = world[worldInternal].readChangesSince(start.world) as WorldChangeRead;
209
- const sharedRead = sharedRefs.readChangesSince(start.sharedRefs);
210
- const next = captureCursor();
211
- if (worldRead.status === 'overflow' || sharedRead.status === 'overflow') {
212
- const oldestAvailable = Math.min(
213
- worldRead.status === 'overflow' ? worldRead.oldestAvailable : Number.MAX_SAFE_INTEGER,
214
- sharedRead.status === 'overflow' ? sharedRead.oldestAvailable : Number.MAX_SAFE_INTEGER,
215
- );
170
+ const toEpoch = world[worldInternal].getMutationEpoch() as number;
171
+ const componentEpochs = world[
172
+ worldInternal
173
+ ].getComponentMutationEpochs() as readonly number[];
174
+ const changedComponentIds: number[] = [];
175
+ for (let componentId = 0; componentId < componentEpochs.length; componentId += 1) {
176
+ const epoch = componentEpochs[componentId] ?? 0;
177
+ if (epoch > start.mutationEpoch && epoch <= toEpoch) changedComponentIds.push(componentId);
178
+ }
179
+ const worldRead: RenderWorldChanges = {
180
+ fromEpoch: start.mutationEpoch,
181
+ toEpoch,
182
+ changedComponentIds,
183
+ };
184
+ const sharedRead = sharedRefs.readChangesSince(start.sharedRefEpoch);
185
+ const version = captureVersion();
186
+ const structureChanged = start.structureEpoch !== world[worldInternal].getStructureEpoch();
187
+ if (structureChanged) {
216
188
  return {
217
- status: 'overflow',
218
- cursor: next,
189
+ status: 'rebuild',
190
+ version,
219
191
  resync: true,
220
- oldestAvailable,
192
+ reason: 'structure-changed',
221
193
  world: worldRead,
222
194
  sharedRefs: sharedRead,
223
195
  };
224
196
  }
225
- return { status: 'ok', cursor: next, world: worldRead, sharedRefs: sharedRead };
197
+ return { status: 'ok', version, world: worldRead, sharedRefs: sharedRead };
226
198
  },
227
199
  querySpans(request: RenderProjectionRequest): RenderProjectionSpans {
228
200
  assertLive();
229
- return readProjectionSpans(world, generation, request);
201
+ return readProjectionSpans(
202
+ world,
203
+ Math.max(1, world[worldInternal].getStructureEpoch()),
204
+ request,
205
+ );
230
206
  },
231
207
  dispose(): void {
232
208
  disposed = true;
233
- cursors.clear();
234
209
  },
235
210
  };
236
211
  }
237
212
 
238
213
  /**
239
- * Publish one owner-derived component value without confusing it with an
240
- * authored mutation. Owner packages use this narrow seam for fields such as
241
- * `Transform.world`: the write still passes through ECS validation and managed
242
- * storage, while the journal records a `derived-component-changed` fact for
243
- * incremental projections.
214
+ * Publish one owner-derived component value through the component's ordinary
215
+ * version. Numeric consumers observe the same row epoch regardless of which
216
+ * owner computed the value; there is no parallel derived-change vocabulary.
244
217
  */
245
218
  export function setDerivedComponent(
246
219
  world: World,
@@ -250,7 +223,7 @@ export function setDerivedComponent(
250
223
  ): Result<void, EcsError> {
251
224
  const result = world[worldInternal].setQueryRow(entity, component, value);
252
225
  if (!result.ok) return result;
253
- world[worldInternal].markDerivedComponentChanges(componentId(component), [entity]);
226
+ world[worldInternal].markComponentChanged(entity, componentId(component));
254
227
  return result;
255
228
  }
256
229
 
@@ -262,57 +235,3 @@ export function routeWorldError(
262
235
  ): void {
263
236
  world[worldInternal].routeError(error, context);
264
237
  }
265
-
266
- /**
267
- * Create the narrow incremental evidence surface for an owner projection.
268
- * Rebuilds intentionally return a reason; complete data always comes from
269
- * the owner's regular Query/QuerySpan path.
270
- */
271
- export function createWorldProjection(
272
- world: World,
273
- options: WorldProjectionOptions = {},
274
- ): WorldProjection {
275
- const componentById =
276
- options.components === undefined
277
- ? undefined
278
- : new Map(options.components.map((component) => [componentId(component), component]));
279
- const componentIds = componentById === undefined ? undefined : new Set(componentById.keys());
280
- let cursor = world[worldInternal].getChangeCursor();
281
-
282
- const pollSince = (requestedCursor: number): ProjectionRead => {
283
- const result = world[worldInternal].readChangesSince(requestedCursor);
284
- if (result.status === 'overflow') {
285
- cursor = result.cursor;
286
- return { status: 'rebuild', cursor, reason: 'journal-overflow' };
287
- }
288
- cursor = result.cursor;
289
- const changes: ProjectionChange[] = [];
290
- for (const record of result.records) {
291
- if (
292
- componentIds !== undefined &&
293
- record.kind !== 'entity-removed' &&
294
- (record.componentId === undefined || !componentIds.has(record.componentId))
295
- ) {
296
- continue;
297
- }
298
- const component =
299
- record.componentId === undefined ? undefined : componentById?.get(record.componentId);
300
- changes.push({
301
- entity: record.entity,
302
- kind: record.kind,
303
- ...(component === undefined ? {} : { component }),
304
- ...(record.componentId === undefined ? {} : { componentId: record.componentId }),
305
- });
306
- }
307
- return { status: 'delta', cursor, changes };
308
- };
309
-
310
- return {
311
- get cursor(): number {
312
- return cursor;
313
- },
314
- poll(): ProjectionRead {
315
- return pollSince(cursor);
316
- },
317
- };
318
- }