@codehz/ecs 0.7.1 → 0.7.3

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 (82) hide show
  1. package/{builder.d.mts → dist/builder.d.mts} +4 -2
  2. package/{world.mjs → dist/world.mjs} +9 -30
  3. package/dist/world.mjs.map +1 -0
  4. package/examples/advanced-scheduling.ts +96 -0
  5. package/examples/collision-detection.ts +229 -0
  6. package/examples/inventory-system-relations.ts +108 -0
  7. package/examples/parent-child-hierarchy.ts +206 -0
  8. package/examples/serialization.ts +337 -0
  9. package/examples/simple.ts +96 -0
  10. package/examples/spatial-grid.ts +276 -0
  11. package/examples/state-machine.ts +273 -0
  12. package/examples/tag-filtering.ts +266 -0
  13. package/package.json +58 -12
  14. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  15. package/src/__tests__/commands/buffer.test.ts +195 -0
  16. package/src/__tests__/component/singleton.test.ts +148 -0
  17. package/src/__tests__/core/archetype.test.ts +247 -0
  18. package/src/__tests__/core/bitset.test.ts +171 -0
  19. package/src/__tests__/core/changeset.test.ts +254 -0
  20. package/src/__tests__/core/multi-map.test.ts +74 -0
  21. package/src/__tests__/entity/component-registry.test.ts +66 -0
  22. package/src/__tests__/entity/entity.test.ts +520 -0
  23. package/src/__tests__/entity/id-manager.test.ts +157 -0
  24. package/src/__tests__/entity/id-system.test.ts +260 -0
  25. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  26. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  27. package/src/__tests__/query/basic.test.ts +341 -0
  28. package/src/__tests__/query/caching.test.ts +112 -0
  29. package/src/__tests__/query/filter.test.ts +111 -0
  30. package/src/__tests__/query/optional.test.ts +231 -0
  31. package/src/__tests__/query/perf.test.ts +99 -0
  32. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  33. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  34. package/src/__tests__/relations/wildcard.test.ts +179 -0
  35. package/src/__tests__/serialization/bounds.test.ts +237 -0
  36. package/src/__tests__/testing/assertions.test.ts +224 -0
  37. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  38. package/src/__tests__/testing/snapshot.test.ts +150 -0
  39. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  40. package/src/__tests__/world/component-hooks.test.ts +185 -0
  41. package/src/__tests__/world/component-management.test.ts +447 -0
  42. package/src/__tests__/world/entity-management.test.ts +86 -0
  43. package/src/__tests__/world/get-optional.test.ts +96 -0
  44. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  45. package/src/__tests__/world/perf.test.ts +93 -0
  46. package/src/__tests__/world/query.test.ts +223 -0
  47. package/src/__tests__/world/serialize.test.ts +83 -0
  48. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  49. package/src/archetype/archetype.ts +472 -0
  50. package/src/archetype/helpers.ts +186 -0
  51. package/src/archetype/store.ts +33 -0
  52. package/src/commands/buffer.ts +110 -0
  53. package/src/commands/changeset.ts +104 -0
  54. package/src/component/entity-store.ts +223 -0
  55. package/src/component/registry.ts +657 -0
  56. package/src/component/type-utils.ts +9 -0
  57. package/src/entity/index.ts +63 -0
  58. package/src/entity/manager.ts +115 -0
  59. package/src/entity/relation.ts +319 -0
  60. package/src/entity/types.ts +135 -0
  61. package/src/index.ts +41 -0
  62. package/src/query/filter.ts +75 -0
  63. package/src/query/query.ts +313 -0
  64. package/src/query/registry.ts +101 -0
  65. package/src/storage/serialization.ts +130 -0
  66. package/src/testing/index.ts +634 -0
  67. package/src/types/index.ts +99 -0
  68. package/src/utils/bit-set.ts +133 -0
  69. package/src/utils/multi-map.ts +96 -0
  70. package/src/utils/utils.ts +19 -0
  71. package/src/world/builder.ts +100 -0
  72. package/src/world/commands.ts +378 -0
  73. package/src/world/hooks.ts +358 -0
  74. package/src/world/references.ts +38 -0
  75. package/src/world/serialization.ts +122 -0
  76. package/src/world/world.ts +1201 -0
  77. package/world.mjs.map +0 -1
  78. /package/{index.d.mts → dist/index.d.mts} +0 -0
  79. /package/{index.mjs → dist/index.mjs} +0 -0
  80. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  81. /package/{testing.mjs → dist/testing.mjs} +0 -0
  82. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
@@ -0,0 +1,110 @@
1
+ import type { EntityId } from "../entity";
2
+
3
+ /**
4
+ * Maximum number of command buffer execution iterations to prevent infinite loops
5
+ */
6
+ const MAX_COMMAND_ITERATIONS = 100;
7
+
8
+ /**
9
+ * Command for deferred execution
10
+ * Uses discriminated union for type safety
11
+ */
12
+ export type Command =
13
+ | { type: "set"; entityId: EntityId; componentType: EntityId<any>; component: any }
14
+ | { type: "delete"; entityId: EntityId; componentType: EntityId<any> }
15
+ | { type: "destroy"; entityId: EntityId };
16
+
17
+ /**
18
+ * Command buffer for deferred structural changes
19
+ */
20
+ export class CommandBuffer {
21
+ private commands: Command[] = [];
22
+ private swapBuffer: Command[] = [];
23
+ /** Reusable map to group commands by entity, avoids per-sync allocations */
24
+ private entityCommands: Map<EntityId, Command[]> = new Map();
25
+ private executeEntityCommands: (entityId: EntityId, commands: Command[]) => void;
26
+
27
+ /**
28
+ * Create a command buffer with an executor function
29
+ */
30
+ constructor(executeEntityCommands: (entityId: EntityId, commands: Command[]) => void) {
31
+ this.executeEntityCommands = executeEntityCommands;
32
+ }
33
+
34
+ /**
35
+ * Add a component to an entity (deferred)
36
+ */
37
+ set(entityId: EntityId, componentType: EntityId<void>): void;
38
+ set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
39
+ set(entityId: EntityId, componentType: EntityId, component?: any): void {
40
+ this.commands.push({ type: "set", entityId, componentType, component });
41
+ }
42
+
43
+ /**
44
+ * Remove a component from an entity (deferred)
45
+ */
46
+ remove<T>(entityId: EntityId, componentType: EntityId<T>): void {
47
+ this.commands.push({ type: "delete", entityId, componentType });
48
+ }
49
+
50
+ /**
51
+ * Destroy an entity (deferred)
52
+ */
53
+ delete(entityId: EntityId): void {
54
+ this.commands.push({ type: "destroy", entityId });
55
+ }
56
+
57
+ /**
58
+ * Execute all commands and clear the buffer
59
+ */
60
+ execute(): void {
61
+ let iterations = 0;
62
+
63
+ while (this.commands.length > 0) {
64
+ if (iterations >= MAX_COMMAND_ITERATIONS) {
65
+ throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
66
+ }
67
+ iterations++;
68
+
69
+ // Swap buffers to avoid allocation
70
+ const currentCommands = this.commands;
71
+ this.commands = this.swapBuffer;
72
+
73
+ // Group commands by entity, reusing the persistent Map
74
+ const entityCommands = this.entityCommands;
75
+ for (const cmd of currentCommands) {
76
+ const existing = entityCommands.get(cmd.entityId);
77
+ if (existing !== undefined) {
78
+ existing.push(cmd);
79
+ } else {
80
+ entityCommands.set(cmd.entityId, [cmd]);
81
+ }
82
+ }
83
+
84
+ // Clear the consumed buffer for reuse
85
+ currentCommands.length = 0;
86
+ this.swapBuffer = currentCommands;
87
+
88
+ // Process each entity's commands and clear the map (but not the arrays,
89
+ // as callers may hold references to them after the executor returns)
90
+ for (const [entityId, commands] of entityCommands) {
91
+ this.executeEntityCommands(entityId, commands);
92
+ }
93
+ entityCommands.clear();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get current commands (for testing)
99
+ */
100
+ getCommands(): Command[] {
101
+ return [...this.commands];
102
+ }
103
+
104
+ /**
105
+ * Clear all commands
106
+ */
107
+ clear(): void {
108
+ this.commands = [];
109
+ }
110
+ }
@@ -0,0 +1,104 @@
1
+ import type { EntityId } from "../entity";
2
+
3
+ /**
4
+ * @internal Represents a set of component changes to be applied to an entity
5
+ */
6
+ export class ComponentChangeset {
7
+ readonly adds = new Map<EntityId<any>, any>();
8
+ readonly removes = new Set<EntityId<any>>();
9
+
10
+ /**
11
+ * Add a component to the changeset
12
+ */
13
+ set<T>(componentType: EntityId<T>, component: T): void {
14
+ this.adds.set(componentType, component);
15
+ this.removes.delete(componentType); // Remove from removes if it was going to be removed
16
+ }
17
+
18
+ /**
19
+ * Remove a component from the changeset
20
+ */
21
+ delete<T>(componentType: EntityId<T>): void {
22
+ this.removes.add(componentType);
23
+ this.adds.delete(componentType); // Remove from adds if it was going to be added
24
+ }
25
+
26
+ /**
27
+ * Check if the changeset has any changes
28
+ */
29
+ hasChanges(): boolean {
30
+ return this.adds.size > 0 || this.removes.size > 0;
31
+ }
32
+
33
+ /**
34
+ * Clear all changes
35
+ */
36
+ clear(): void {
37
+ this.adds.clear();
38
+ this.removes.clear();
39
+ }
40
+
41
+ /**
42
+ * Merge another changeset into this one
43
+ */
44
+ merge(other: ComponentChangeset): void {
45
+ // Merge additions
46
+ for (const [componentType, component] of other.adds) {
47
+ this.adds.set(componentType, component);
48
+ this.removes.delete(componentType);
49
+ }
50
+ // Merge removals
51
+ for (const componentType of other.removes) {
52
+ this.removes.add(componentType);
53
+ this.adds.delete(componentType);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Apply the changeset to existing components and return the final state
59
+ */
60
+ applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any> {
61
+ // Apply removals
62
+ for (const componentType of this.removes) {
63
+ existingComponents.delete(componentType);
64
+ }
65
+
66
+ // Apply additions/updates
67
+ for (const [componentType, component] of this.adds) {
68
+ existingComponents.set(componentType, component);
69
+ }
70
+
71
+ return existingComponents;
72
+ }
73
+
74
+ /**
75
+ * Get the final component types after applying the changeset
76
+ * @param existingComponentTypes - The current component types on the entity
77
+ * @returns The final component types or undefined if no changes
78
+ */
79
+ getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined {
80
+ const finalComponentTypes = new Set<EntityId<any>>(existingComponentTypes);
81
+ let changed = false;
82
+
83
+ // Apply removals
84
+ for (const componentType of this.removes) {
85
+ if (!finalComponentTypes.has(componentType)) {
86
+ this.removes.delete(componentType);
87
+ continue; // Component not present, skip
88
+ }
89
+ changed = true;
90
+ finalComponentTypes.delete(componentType);
91
+ }
92
+
93
+ // Apply additions
94
+ for (const componentType of this.adds.keys()) {
95
+ if (finalComponentTypes.has(componentType)) {
96
+ continue; // Component already present, skip
97
+ }
98
+ changed = true;
99
+ finalComponentTypes.add(componentType);
100
+ }
101
+
102
+ return changed ? Array.from(finalComponentTypes) : undefined;
103
+ }
104
+ }
@@ -0,0 +1,223 @@
1
+ import { hasWildcardRelation } from "../archetype/helpers";
2
+ import type { Command } from "../commands/buffer";
3
+ import {
4
+ getComponentIdFromRelationId,
5
+ getComponentMerge,
6
+ getDetailedIdType,
7
+ isWildcardRelationId,
8
+ type ComponentId,
9
+ type EntityId,
10
+ type WildcardRelationId,
11
+ } from "../entity";
12
+
13
+ /**
14
+ * Manages component entity (singleton) storage.
15
+ *
16
+ * Component entities use a flat Map-based storage rather than the Archetype-based
17
+ * storage used for regular entities. Their IDs are in the component ID range
18
+ * (or are relation IDs), distinguishing them from regular entity IDs.
19
+ */
20
+ export class ComponentEntityStore {
21
+ private readonly componentEntityComponents: Map<EntityId, Map<EntityId<any>, any>> = new Map();
22
+ private readonly relationEntityIdsByTarget: Map<EntityId, Set<EntityId>> = new Map();
23
+
24
+ /**
25
+ * Check if an entity ID is a component entity type.
26
+ * Returns true for component IDs, component-relation IDs, and entity-relation IDs —
27
+ * i.e. anything that is NOT a plain entity or an invalid ID.
28
+ */
29
+ exists(entityId: EntityId): boolean {
30
+ const detailed = getDetailedIdType(entityId);
31
+ return detailed.type !== "entity" && detailed.type !== "invalid";
32
+ }
33
+
34
+ /**
35
+ * Check if a component entity has a specific component.
36
+ */
37
+ has(entityId: EntityId, componentType: EntityId<any>): boolean {
38
+ return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
39
+ }
40
+
41
+ /**
42
+ * Check if a singleton component has data — the has(componentId) overload.
43
+ * In singleton usage the entity ID and the component type are the same value.
44
+ */
45
+ hasSingleton(componentId: EntityId<any>): boolean {
46
+ return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
47
+ }
48
+
49
+ /**
50
+ * Check if a component entity has any wildcard relations matching a component ID.
51
+ */
52
+ hasWildcard(entityId: EntityId, componentId: ComponentId<any>): boolean {
53
+ const data = this.componentEntityComponents.get(entityId);
54
+ if (!data) return false;
55
+ return hasWildcardRelation(data, componentId);
56
+ }
57
+
58
+ /**
59
+ * Get a component value from a component entity.
60
+ * Throws if the component does not exist.
61
+ */
62
+ get<T>(entityId: EntityId, componentType: EntityId<T>): T {
63
+ const data = this.componentEntityComponents.get(entityId);
64
+ if (!data || !data.has(componentType)) {
65
+ throw new Error(
66
+ `Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`,
67
+ );
68
+ }
69
+ return data.get(componentType) as T;
70
+ }
71
+
72
+ /**
73
+ * Get an optional component value from a component entity.
74
+ * Returns undefined if the component does not exist.
75
+ */
76
+ getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
77
+ const data = this.componentEntityComponents.get(entityId);
78
+ if (!data || !data.has(componentType)) return undefined;
79
+ return { value: data.get(componentType) as T };
80
+ }
81
+
82
+ /**
83
+ * Get all wildcard relations of a given type from a component entity.
84
+ */
85
+ getWildcard<T>(entityId: EntityId, wildcardComponentType: WildcardRelationId<T>): [EntityId<unknown>, T][] {
86
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
87
+ const data = this.componentEntityComponents.get(entityId);
88
+ if (componentId === undefined || !data) return [];
89
+
90
+ const relations: [EntityId<unknown>, T][] = [];
91
+ for (const [key, value] of data.entries()) {
92
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
93
+ const detailed = getDetailedIdType(key);
94
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") {
95
+ relations.push([detailed.targetId, value as T]);
96
+ }
97
+ }
98
+ return relations;
99
+ }
100
+
101
+ /**
102
+ * Clear all data for a component entity.
103
+ */
104
+ clear(entityId: EntityId): void {
105
+ if (this.componentEntityComponents.delete(entityId)) {
106
+ this.unregisterRelationEntityId(entityId);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Cleanup all component entities that reference a given target entity.
112
+ * Called when a target entity is destroyed.
113
+ */
114
+ cleanupReferencesTo(targetId: EntityId): void {
115
+ const relationEntities = this.relationEntityIdsByTarget.get(targetId);
116
+ if (!relationEntities) return;
117
+ for (const relationEntityId of relationEntities) {
118
+ this.componentEntityComponents.delete(relationEntityId);
119
+ }
120
+ this.relationEntityIdsByTarget.delete(targetId);
121
+ }
122
+
123
+ /**
124
+ * Execute a batch of commands for a component entity.
125
+ */
126
+ executeCommands(entityId: EntityId, commands: Command[]): void {
127
+ if (commands.some((cmd) => cmd.type === "destroy")) {
128
+ this.clear(entityId);
129
+ return;
130
+ }
131
+
132
+ const pendingSetValues = new Map<EntityId<any>, any>();
133
+
134
+ for (const command of commands) {
135
+ if (command.type === "set" && command.componentType) {
136
+ const merge = getComponentMerge(command.componentType);
137
+ let nextValue = command.component;
138
+ if (merge !== undefined && pendingSetValues.has(command.componentType)) {
139
+ const prevValue = pendingSetValues.get(command.componentType);
140
+ nextValue = merge(prevValue, command.component);
141
+ }
142
+ pendingSetValues.set(command.componentType, nextValue);
143
+
144
+ let data = this.componentEntityComponents.get(entityId);
145
+ if (!data) {
146
+ data = new Map();
147
+ this.componentEntityComponents.set(entityId, data);
148
+ this.registerRelationEntityId(entityId);
149
+ }
150
+ data.set(command.componentType, nextValue);
151
+ } else if (command.type === "delete" && command.componentType) {
152
+ const data = this.componentEntityComponents.get(entityId);
153
+
154
+ if (isWildcardRelationId(command.componentType)) {
155
+ const componentId = getComponentIdFromRelationId(command.componentType);
156
+ if (componentId !== undefined) {
157
+ if (data) {
158
+ for (const key of Array.from(data.keys())) {
159
+ if (getComponentIdFromRelationId(key) === componentId) {
160
+ data.delete(key);
161
+ }
162
+ }
163
+ }
164
+ for (const key of Array.from(pendingSetValues.keys())) {
165
+ if (getComponentIdFromRelationId(key) === componentId) {
166
+ pendingSetValues.delete(key);
167
+ }
168
+ }
169
+ }
170
+ } else {
171
+ data?.delete(command.componentType);
172
+ pendingSetValues.delete(command.componentType);
173
+ }
174
+
175
+ if (data?.size === 0) {
176
+ this.clear(entityId);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Initialize a component entity from a deserialization snapshot.
184
+ */
185
+ initFromSnapshot(entityId: EntityId, componentMap: Map<EntityId<any>, any>): void {
186
+ this.componentEntityComponents.set(entityId, componentMap);
187
+ this.registerRelationEntityId(entityId);
188
+ }
189
+
190
+ /**
191
+ * Iterate over all component entity entries.
192
+ * Used for serialization.
193
+ */
194
+ entries(): IterableIterator<[EntityId, Map<EntityId<any>, any>]> {
195
+ return this.componentEntityComponents.entries();
196
+ }
197
+
198
+ private registerRelationEntityId(entityId: EntityId): void {
199
+ const detailed = getDetailedIdType(entityId);
200
+ if (detailed.type !== "entity-relation") return;
201
+ const targetId = detailed.targetId;
202
+ if (targetId === undefined) return;
203
+ const existing = this.relationEntityIdsByTarget.get(targetId);
204
+ if (existing) {
205
+ existing.add(entityId);
206
+ return;
207
+ }
208
+ this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
209
+ }
210
+
211
+ private unregisterRelationEntityId(entityId: EntityId): void {
212
+ const detailed = getDetailedIdType(entityId);
213
+ if (detailed.type !== "entity-relation") return;
214
+ const targetId = detailed.targetId;
215
+ if (targetId === undefined) return;
216
+ const existing = this.relationEntityIdsByTarget.get(targetId);
217
+ if (!existing) return;
218
+ existing.delete(entityId);
219
+ if (existing.size === 0) {
220
+ this.relationEntityIdsByTarget.delete(targetId);
221
+ }
222
+ }
223
+ }