Skip to content

Commit 81e3d07

Browse files
committed
Merge branch 'main' into chore/add-exports
2 parents 2eb35cb + a33233e commit 81e3d07

File tree

10 files changed

+98
-11
lines changed

10 files changed

+98
-11
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ These are more like notes for docs. Take a look around, ask questions. Eventuall
447447
448448
This is where all data is stored. We have methods on entities but this is a bit of a trick, entities don't actually store any data and instead it is operating on the connected world. Each world has its own set of entities that do not overlap with another. Typically you only need one world.
449449
450-
Worlds can have traits, which is our version of a singleton. Use these for global resources like a clock. Each world gets its own entity used for world traits. This entity is no queryable but will show up in the list of active entities making the only way to retrieve a world trait with its API.
450+
Worlds can have traits, which is our version of a singleton. Use these for global resources like a clock. Each world gets its own entity used for world traits. This entity is not queryable but will show up in the list of active entities making the only way to retrieve a world trait with its API.
451451
452452
```js
453453
// Spawns an entity
@@ -533,7 +533,7 @@ entity.remove(Position)
533533

534534
// Checks if the entity has the trait
535535
// Return boolean
536-
const result = enttiy.has(Position)
536+
const result = entity.has(Position)
537537

538538
// Gets a snapshot instance of the trait
539539
// Return TraitInstance

packages/core/src/query/modifiers/changed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export function setChanged(world: World, entity: Entity, trait: Trait) {
4343
for (const query of data.queries) {
4444
// If the query has no changed modifiers, continue.
4545
if (!query.hasChangedModifiers) continue;
46+
// If the trait is not part of a Changed modifier in this query, continue.
47+
if (!query.changedTraits.has(trait)) continue;
4648

4749
// Check if the entity matches the query.
4850
let match = query.check(world, entity, { type: 'change', traitData: data });

packages/core/src/query/query-result.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,16 @@ export function createQueryResult<T extends QueryParameter[]>(
2323
): QueryResult<T> {
2424
query.commitRemovals(world);
2525
const entities = query.entities.dense.slice() as Entity[];
26+
2627
// Clear so it can accumulate again.
27-
if (query.isTracking) query.entities.clear();
28+
if (query.isTracking) {
29+
query.entities.clear();
30+
31+
// @todo: Need to improve the performance of this loop.
32+
for (const eid of entities) {
33+
query.resetTrackingBitmasks(eid);
34+
}
35+
}
2836

2937
const stores: Store<any>[] = [];
3038
const traits: Trait[] = [];

packages/core/src/query/query.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,13 @@ export class Query {
290290
const result = this.entities.dense.slice();
291291

292292
// Clear so it can accumulate again.
293-
if (this.isTracking) this.entities.clear();
293+
if (this.isTracking) {
294+
this.entities.clear();
295+
296+
for (const eid of result) {
297+
this.resetTrackingBitmasks(eid);
298+
}
299+
}
294300

295301
return result as Entity[];
296302
}

packages/core/src/world/world.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,13 @@ export class World {
128128
const ctx = this[$internal];
129129

130130
// Destroy all entities so any cleanup is done.
131-
this.entities.forEach((entity) => destroyEntity(this, entity));
131+
this.entities.forEach((entity) => {
132+
// Some relations may have caused the entity to be destroyed before
133+
// we get to them in the loop.
134+
if (this.has(entity)) {
135+
destroyEntity(this, entity)
136+
}
137+
});
132138

133139
ctx.entityIndex = createEntityIndex(this.#id);
134140
ctx.entityTraits.clear();

packages/core/tests/query-modifiers.test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from '../src';
1212

1313
const Position = trait({ x: 0, y: 0 });
14-
const Name = trait({ name: 'name' });
1514
const IsActive = trait();
1615
const Foo = trait({});
1716
const Bar = trait({});
@@ -382,7 +381,7 @@ describe('Query modifiers', () => {
382381
entityA.remove(Foo);
383382
entityA.add(Foo);
384383
entities = world.query(Added(Foo), Removed(Bar));
385-
expect(entities[0]).toBe(entityA);
384+
expect(entities.length).toBe(0);
386385

387386
// Add Foo to entityB and remove Bar.
388387
// This entity should now match the query.
@@ -463,4 +462,40 @@ describe('Query modifiers', () => {
463462
expect(entities.length).toBe(1);
464463
expect(entities2.length).toBe(1);
465464
});
465+
466+
it('should only update a Changed query when the tracked trait is changed', () => {
467+
const entity = world.spawn(Foo, Bar);
468+
469+
const Changed = createChanged();
470+
471+
expect(world.queryFirst(Changed(Foo), Changed(Bar))).toBeUndefined();
472+
473+
entity.changed(Foo);
474+
entity.changed(Bar);
475+
expect(world.queryFirst(Changed(Foo), Changed(Bar))).toBe(entity);
476+
477+
entity.changed(Foo);
478+
expect(world.queryFirst(Changed(Foo), Changed(Bar))).toBeUndefined();
479+
});
480+
481+
// @see https://github.com/pmndrs/koota/issues/115
482+
it('should not trigger Changed query when removing a different trait', () => {
483+
const Changed = createChanged();
484+
const entity = world.spawn(Position);
485+
486+
// Initial state - no changes
487+
expect(world.queryFirst(Changed(Position), Not(Foo))).toBeUndefined();
488+
489+
// Change Position
490+
entity.changed(Position);
491+
expect(world.queryFirst(Changed(Position), Not(Foo))).toBe(entity);
492+
493+
// Query again - should be empty
494+
expect(world.queryFirst(Changed(Position), Not(Foo))).toBeUndefined();
495+
496+
// Add and remove Foo - should be empty
497+
entity.add(Foo);
498+
entity.remove(Foo);
499+
expect(world.queryFirst(Changed(Position), Not(Foo))).toBeUndefined();
500+
});
466501
});

packages/core/tests/world.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeEach, describe, expect, it } from 'vitest';
2-
import { createWorld, trait, TraitInstance, universe } from '../src';
2+
import { createWorld, relation, trait, TraitInstance, universe } from '../src';
33

44
describe('World', () => {
55
beforeEach(() => {
@@ -24,6 +24,24 @@ describe('World', () => {
2424
expect(world.entities.length).toBe(1);
2525
});
2626

27+
it('reset should remove entities with auto-remove relations', () => {
28+
const Node = trait();
29+
const ChildOf = relation({ autoRemoveTarget: true, exclusive: true });
30+
31+
const world = createWorld();
32+
33+
// Create a parent node and a child node.
34+
const parentNode = world.spawn(Node);
35+
world.spawn(Node, ChildOf(parentNode));
36+
37+
// Expect this to not throw, since the ChildOf relation will automatically
38+
// remove the child node when the parent node is destroyed first.
39+
expect(() => world.reset()).not.toThrow();
40+
41+
// Always has one entity that is the world itself.
42+
expect(world.entities.length).toBe(1);
43+
});
44+
2745
it('errors if more than 16 worlds are created', () => {
2846
for (let i = 0; i < 16; i++) {
2947
createWorld();

packages/publish/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ These are more like notes for docs. Take a look around, ask questions. Eventuall
447447
448448
This is where all data is stored. We have methods on entities but this is a bit of a trick, entities don't actually store any data and instead it is operating on the connected world. Each world has its own set of entities that do not overlap with another. Typically you only need one world.
449449
450-
Worlds can have traits, which is our version of a singleton. Use these for global resources like a clock. Each world gets its own entity used for world traits. This entity is no queryable but will show up in the list of active entities making the only way to retrieve a world trait with its API.
450+
Worlds can have traits, which is our version of a singleton. Use these for global resources like a clock. Each world gets its own entity used for world traits. This entity is not queryable but will show up in the list of active entities making the only way to retrieve a world trait with its API.
451451
452452
```js
453453
// Spawns an entity
@@ -533,7 +533,7 @@ entity.remove(Position)
533533

534534
// Checks if the entity has the trait
535535
// Return boolean
536-
const result = enttiy.has(Position)
536+
const result = entity.has(Position)
537537

538538
// Gets a snapshot instance of the trait
539539
// Return TraitInstance

packages/publish/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "koota",
3-
"version": "0.4.0",
3+
"version": "0.4.2",
44
"description": "🌎 Performant real-time state management for React and TypeScript",
55
"license": "ISC",
66
"type": "module",

packages/publish/tsup.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { defineConfig } from 'tsup';
33
export default defineConfig({
44
entry: ['src/index.ts', 'src/react.ts'],
55
format: ['esm', 'cjs'],
6+
// Force emitting "use strict" for ESM output
7+
// Not all bundlers and frameworks are capable of correctly transforming esm
8+
// to cjs output and koota requires strict mode to be enabled for the code to
9+
// be sound. The "use strict" directive has no ill effect when running in an
10+
// esm environment, while bringing the extra guarantee of ensuring strict mode
11+
// is used in non-conformant environments.
12+
// See https://262.ecma-international.org/5.1/#sec-C for more details.
13+
esbuildOptions: (options, { format }) => {
14+
options.banner = format === 'esm' ? {
15+
js: '\"use strict\";',
16+
} : undefined;
17+
},
618
dts: {
719
resolve: true,
820
},

0 commit comments

Comments
 (0)