@codehz/ecs 0.7.5 → 0.7.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "license": "MIT",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -27,8 +27,8 @@
27
27
  "README.en.md"
28
28
  ],
29
29
  "devDependencies": {
30
- "@codehz/pipeline": "^0.3.0",
31
- "@types/bun": "1.3.2",
30
+ "@codehz/pipeline": "^0.4.3",
31
+ "@types/bun": "1.3.13",
32
32
  "@typescript-eslint/eslint-plugin": "^8.48.1",
33
33
  "@typescript-eslint/parser": "^8.48.1",
34
34
  "eslint": "^9.39.1",
@@ -37,7 +37,7 @@
37
37
  "lint-staged": "^16.2.6",
38
38
  "prettier": "^3.6.2",
39
39
  "prettier-plugin-organize-imports": "^4.3.0",
40
- "tsdown": "^0.16.6"
40
+ "tsdown": "^0.22.0"
41
41
  },
42
42
  "repository": {
43
43
  "url": "https://github.com/codehz/ecs"
@@ -329,4 +329,45 @@ describe("Wildcard-Relation Hooks", () => {
329
329
  expect(world.exists(entity1)).toBe(true);
330
330
  expect(world.exists(entity2)).toBe(true);
331
331
  });
332
+
333
+ // Regression: wildcard remove (relation(Comp, "*")) should NOT produce target=0
334
+ // in on_remove. The wildcard marker (WILDCARD_TARGET_ID=0) leaks into
335
+ // removedComponents and gets decoded as target=0 by reconstructWildcardWithRemoved.
336
+ it("should NOT report target=0 in on_remove when using wildcard remove (relation(Comp, '*'))", () => {
337
+ const world = new World();
338
+
339
+ // Use dontFragment:true so that removeWildcardRelations adds the wildcard
340
+ // marker to removedComponents, triggering the bug path.
341
+ const RelData = component<{ value: string }>({ dontFragment: true });
342
+ const target = world.new();
343
+ const wildcardRel = relation(RelData, "*");
344
+ const concreteRel = relation(RelData, target);
345
+
346
+ const removeCalls: { relations: [EntityId, { value: string }][] }[] = [];
347
+
348
+ world.hook([wildcardRel], {
349
+ on_remove: (_entityId, relations) => {
350
+ removeCalls.push({ relations });
351
+ },
352
+ });
353
+
354
+ const entity = world.spawn().with(concreteRel, { value: "hello" }).build();
355
+ world.sync();
356
+
357
+ // Remove ALL relations via wildcard — this triggers the bug
358
+ world.remove(entity, wildcardRel);
359
+ world.sync();
360
+
361
+ expect(removeCalls.length).toBe(1);
362
+ const reportedRelations = removeCalls[0]!.relations;
363
+
364
+ // The hook should report the removed relation(s) WITHOUT the wildcard
365
+ // marker leaking in as target=0.
366
+ for (const [reportedTarget] of reportedRelations) {
367
+ expect(reportedTarget).not.toBe(0);
368
+ }
369
+
370
+ // It should still correctly report the actual removed relation
371
+ expect(reportedRelations).toContainEqual([target, { value: "hello" }]);
372
+ });
332
373
  });
@@ -263,6 +263,9 @@ function reconstructWildcardWithRemoved(
263
263
  // Re-inject matching relations that were just removed, so the hook callback
264
264
  // sees the complete snapshot as it existed before the removal.
265
265
  for (const [removedCompId, removedValue] of removedComponents.entries()) {
266
+ // Skip wildcard markers themselves — they encode WILDCARD_TARGET_ID=0 and
267
+ // would produce spurious [0, undefined] entries in the hook callback.
268
+ if (isWildcardRelationId(removedCompId)) continue;
266
269
  if (componentMatchesHookType(removedCompId, wildcardId)) {
267
270
  const targetId = getTargetIdFromRelationId(removedCompId);
268
271
  if (targetId !== undefined) {
@@ -346,6 +349,8 @@ function collectWildcardFromRemoved(
346
349
  const result: [EntityId, any][] = [];
347
350
 
348
351
  for (const [removedCompId, removedValue] of removedComponents.entries()) {
352
+ // Skip wildcard markers themselves — they encode WILDCARD_TARGET_ID=0.
353
+ if (isWildcardRelationId(removedCompId)) continue;
349
354
  if (componentMatchesHookType(removedCompId, wildcardId)) {
350
355
  const targetId = getTargetIdFromRelationId(removedCompId);
351
356
  if (targetId !== undefined) {