@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
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { componentId, defineComponent } from '../component';
|
|
3
|
-
import { encodeEntity } from '../entity-handle';
|
|
4
|
-
import { createWorldProjection } from '../projection';
|
|
5
|
-
import { World } from '../world';
|
|
6
|
-
import { WorldChangeJournal, type WorldChangeRecord } from '../world-change-journal';
|
|
7
|
-
import { worldInternal } from '../world-internal';
|
|
8
|
-
|
|
9
|
-
describe('WorldChangeJournal', () => {
|
|
10
|
-
it('returns only records after the supplied cursor', () => {
|
|
11
|
-
const journal = new WorldChangeJournal(4);
|
|
12
|
-
const cursor = journal.cursor();
|
|
13
|
-
const first = encodeEntity(7, 0);
|
|
14
|
-
const second = encodeEntity(9, 0);
|
|
15
|
-
|
|
16
|
-
journal.append({ kind: 'component-changed', entity: first, componentId: 3 });
|
|
17
|
-
journal.append({ kind: 'entity-removed', entity: second });
|
|
18
|
-
|
|
19
|
-
expect(journal.readAfter(cursor)).toEqual({
|
|
20
|
-
status: 'ok',
|
|
21
|
-
cursor: 2,
|
|
22
|
-
records: [
|
|
23
|
-
{ sequence: 1, kind: 'component-changed', entity: first, componentId: 3 },
|
|
24
|
-
{ sequence: 2, kind: 'entity-removed', entity: second },
|
|
25
|
-
],
|
|
26
|
-
});
|
|
27
|
-
expect(journal.readAfter(2)).toEqual({ status: 'ok', cursor: 2, records: [] });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('reports overflow instead of returning an incomplete delta', () => {
|
|
31
|
-
const journal = new WorldChangeJournal(2);
|
|
32
|
-
const cursor = journal.cursor();
|
|
33
|
-
const first = encodeEntity(1, 0);
|
|
34
|
-
const second = encodeEntity(2, 0);
|
|
35
|
-
const third = encodeEntity(3, 0);
|
|
36
|
-
|
|
37
|
-
journal.append({ kind: 'component-changed', entity: first, componentId: 1 });
|
|
38
|
-
journal.append({ kind: 'component-changed', entity: second, componentId: 1 });
|
|
39
|
-
journal.append({ kind: 'component-changed', entity: third, componentId: 1 });
|
|
40
|
-
|
|
41
|
-
expect(journal.readAfter(cursor)).toEqual({
|
|
42
|
-
status: 'overflow',
|
|
43
|
-
cursor: 3,
|
|
44
|
-
oldestAvailable: 2,
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('World mutation journal', () => {
|
|
50
|
-
it('projects narrow component evidence without exposing journal records', () => {
|
|
51
|
-
const Value = defineComponent('ProjectionValue', { value: 'u32' });
|
|
52
|
-
const world = new World();
|
|
53
|
-
const projection = createWorldProjection(world, { components: [Value] });
|
|
54
|
-
const entity = world.spawn({ component: Value, data: { value: 1 } }).unwrap();
|
|
55
|
-
|
|
56
|
-
const result = projection.poll();
|
|
57
|
-
expect(result.status).toBe('delta');
|
|
58
|
-
if (result.status !== 'delta') return;
|
|
59
|
-
expect(result.changes).toEqual([
|
|
60
|
-
{ entity, kind: 'component-added', component: Value, componentId: componentId(Value) },
|
|
61
|
-
]);
|
|
62
|
-
expect(result.changes[0]).not.toHaveProperty('sequence');
|
|
63
|
-
expect(result.changes[0]).not.toHaveProperty('table');
|
|
64
|
-
expect(result.changes[0]).not.toHaveProperty('column');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('publishes component lifecycle and entity removal as ordered evidence', () => {
|
|
68
|
-
const Marker = defineComponent('WorldJournalMarker', { value: 'f32' });
|
|
69
|
-
const Extra = defineComponent('WorldJournalExtra', { value: 'u32' });
|
|
70
|
-
const world = new World();
|
|
71
|
-
const cursor = world[worldInternal].getChangeCursor();
|
|
72
|
-
|
|
73
|
-
const entity = world.spawn({ component: Marker, data: { value: 1 } }).unwrap();
|
|
74
|
-
world.set(entity, Marker, { value: 2 }).unwrap();
|
|
75
|
-
world.addComponent(entity, { component: Extra, data: { value: 3 } }).unwrap();
|
|
76
|
-
world.removeComponent(entity, Extra).unwrap();
|
|
77
|
-
world.despawn(entity).unwrap();
|
|
78
|
-
|
|
79
|
-
const result = world[worldInternal].readChangesSince(cursor);
|
|
80
|
-
expect(result.status).toBe('ok');
|
|
81
|
-
if (result.status !== 'ok') return;
|
|
82
|
-
expect(
|
|
83
|
-
result.records.map((record: WorldChangeRecord) => ({
|
|
84
|
-
kind: record.kind,
|
|
85
|
-
entity: record.entity,
|
|
86
|
-
componentId: record.componentId,
|
|
87
|
-
})),
|
|
88
|
-
).toEqual([
|
|
89
|
-
{ kind: 'component-added', entity, componentId: 0 },
|
|
90
|
-
{ kind: 'component-added', entity, componentId: componentId(Marker) },
|
|
91
|
-
{ kind: 'component-changed', entity, componentId: componentId(Marker) },
|
|
92
|
-
{ kind: 'component-added', entity, componentId: componentId(Extra) },
|
|
93
|
-
{ kind: 'component-removed', entity, componentId: componentId(Extra) },
|
|
94
|
-
{ kind: 'entity-removed', entity, componentId: undefined },
|
|
95
|
-
]);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
describe('package-private World internal seam', () => {
|
|
4
|
-
it('shares the World key across split bundled entry points', async () => {
|
|
5
|
-
const indexPath = '../../dist/index.mjs';
|
|
6
|
-
const projectionPath = '../../dist/projection/index.mjs';
|
|
7
|
-
const [{ World }, { createWorldProjection }] = await Promise.all([
|
|
8
|
-
import(indexPath),
|
|
9
|
-
import(projectionPath),
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
const world = new World();
|
|
13
|
-
const projection = createWorldProjection(world);
|
|
14
|
-
|
|
15
|
-
expect(projection.poll()).toEqual({ status: 'delta', cursor: 0, changes: [] });
|
|
16
|
-
});
|
|
17
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { EntityHandle } from './entity-handle';
|
|
2
|
-
|
|
3
|
-
export type WorldChangeKind =
|
|
4
|
-
| 'component-added'
|
|
5
|
-
| 'component-changed'
|
|
6
|
-
| 'component-removed'
|
|
7
|
-
| 'derived-component-changed'
|
|
8
|
-
| 'entity-removed';
|
|
9
|
-
|
|
10
|
-
export interface WorldChangeRecord {
|
|
11
|
-
readonly sequence: number;
|
|
12
|
-
readonly kind: WorldChangeKind;
|
|
13
|
-
readonly entity: EntityHandle;
|
|
14
|
-
readonly componentId?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type WorldChange = Omit<WorldChangeRecord, 'sequence'>;
|
|
18
|
-
|
|
19
|
-
export type WorldChangeRead =
|
|
20
|
-
| {
|
|
21
|
-
readonly status: 'ok';
|
|
22
|
-
readonly cursor: number;
|
|
23
|
-
readonly records: readonly WorldChangeRecord[];
|
|
24
|
-
}
|
|
25
|
-
| {
|
|
26
|
-
readonly status: 'overflow';
|
|
27
|
-
readonly cursor: number;
|
|
28
|
-
readonly oldestAvailable: number;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const EMPTY_CHANGES: readonly WorldChangeRecord[] = Object.freeze([]);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Bounded mutation evidence for engine-owned projections.
|
|
35
|
-
*
|
|
36
|
-
* Readers retain a cursor and rebuild their projection when the ring has
|
|
37
|
-
* overwritten unread records. An overflow never returns a partial delta.
|
|
38
|
-
*/
|
|
39
|
-
export class WorldChangeJournal {
|
|
40
|
-
private readonly records: Array<WorldChangeRecord | undefined>;
|
|
41
|
-
private nextSequence = 1;
|
|
42
|
-
|
|
43
|
-
constructor(readonly capacity = 65_536) {
|
|
44
|
-
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
45
|
-
throw new RangeError('WorldChangeJournal capacity must be a positive safe integer');
|
|
46
|
-
}
|
|
47
|
-
this.records = new Array<WorldChangeRecord | undefined>(capacity);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
cursor(): number {
|
|
51
|
-
return this.nextSequence - 1;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
append(change: WorldChange): number {
|
|
55
|
-
const sequence = this.nextSequence;
|
|
56
|
-
this.nextSequence += 1;
|
|
57
|
-
this.records[(sequence - 1) % this.capacity] = { ...change, sequence };
|
|
58
|
-
return sequence;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
readAfter(cursor: number): WorldChangeRead {
|
|
62
|
-
const latest = this.cursor();
|
|
63
|
-
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
|
|
64
|
-
throw new RangeError(`WorldChangeJournal cursor ${cursor} is outside 0..${latest}`);
|
|
65
|
-
}
|
|
66
|
-
const oldestAvailable = Math.max(1, latest - this.capacity + 1);
|
|
67
|
-
if (cursor < oldestAvailable - 1) {
|
|
68
|
-
return { status: 'overflow', cursor: latest, oldestAvailable };
|
|
69
|
-
}
|
|
70
|
-
if (cursor === latest) {
|
|
71
|
-
return { status: 'ok', cursor: latest, records: EMPTY_CHANGES };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const records: WorldChangeRecord[] = [];
|
|
75
|
-
for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
|
|
76
|
-
const record = this.records[(sequence - 1) % this.capacity];
|
|
77
|
-
if (record === undefined || record.sequence !== sequence) {
|
|
78
|
-
return { status: 'overflow', cursor: latest, oldestAvailable };
|
|
79
|
-
}
|
|
80
|
-
records.push(record);
|
|
81
|
-
}
|
|
82
|
-
return { status: 'ok', cursor: latest, records };
|
|
83
|
-
}
|
|
84
|
-
}
|