@latticexyz/recs 2.0.0-next.0 → 2.0.0-next.10

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/src/Query.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { filterNullish } from "@latticexyz/utils";
2
2
  import { observable, ObservableSet } from "mobx";
3
- import { concat, concatMap, filter, from, map, merge, Observable, of, Subject } from "rxjs";
3
+ import { concat, concatMap, filter, from, map, merge, Observable, of, share, Subject } from "rxjs";
4
4
  import {
5
5
  componentValueEquals,
6
6
  getComponentEntities,
@@ -506,22 +506,9 @@ export function defineQuery(
506
506
  filterNullish()
507
507
  );
508
508
 
509
- // Create a new Subject to allow multiple observers
510
- // but only subscribe to the internal stream when the update stream is
511
- // subscribed to, in order to get the same behavior as if exposing the
512
- // internal stream directly (ie only running the internal$ pipe if there are subscribers)
513
- const update$ = new Subject<ComponentUpdate & { type: UpdateType }>();
514
- const world = fragments[0].component.world;
515
- const subscribe = update$.subscribe.bind(update$);
516
- update$.subscribe = (observer) => {
517
- const subscription = internal$.subscribe(update$);
518
- world.registerDisposer(() => subscription?.unsubscribe());
519
- return subscribe(observer as Parameters<typeof subscribe>[0]);
520
- };
521
-
522
509
  return {
523
510
  matching,
524
- update$: concat(initial$, update$),
511
+ update$: concat(initial$, internal$).pipe(share()),
525
512
  };
526
513
  }
527
514
 
@@ -0,0 +1,139 @@
1
+ import { defineComponent, removeComponent, setComponent, withValue } from "./Component";
2
+ import { Type, UpdateType } from "./constants";
3
+ import { createEntity } from "./Entity";
4
+ import { Has } from "./Query";
5
+ import { defineEnterSystem, defineExitSystem, defineSystem, defineUpdateSystem } from "./System";
6
+ import { Component, Entity, World } from "./types";
7
+ import { createWorld } from "./World";
8
+
9
+ describe("System", () => {
10
+ let world: World;
11
+
12
+ beforeEach(() => {
13
+ world = createWorld();
14
+ });
15
+
16
+ describe("Systems", () => {
17
+ let Position: Component<{ x: number; y: number }>;
18
+ let entity: Entity;
19
+
20
+ beforeEach(() => {
21
+ Position = defineComponent(world, { x: Type.Number, y: Type.Number });
22
+ entity = createEntity(world, [withValue(Position, { x: 1, y: 2 })]);
23
+ });
24
+
25
+ it("defineSystem should rerun the system if the query result changes (enter, update, exit)", () => {
26
+ const mock = jest.fn();
27
+ defineSystem(world, [Has(Position)], mock);
28
+
29
+ setComponent(Position, entity, { x: 2, y: 3 });
30
+
31
+ expect(mock).toHaveBeenCalledTimes(2);
32
+ expect(mock).toHaveBeenCalledWith({
33
+ entity,
34
+ component: Position,
35
+ value: [
36
+ { x: 2, y: 3 },
37
+ { x: 1, y: 2 },
38
+ ],
39
+ type: UpdateType.Update,
40
+ });
41
+
42
+ setComponent(Position, entity, { x: 3, y: 3 });
43
+ expect(mock).toHaveBeenCalledTimes(3);
44
+ expect(mock).toHaveBeenCalledWith({
45
+ entity,
46
+ component: Position,
47
+ value: [
48
+ { x: 3, y: 3 },
49
+ { x: 2, y: 3 },
50
+ ],
51
+ type: UpdateType.Update,
52
+ });
53
+
54
+ removeComponent(Position, entity);
55
+ expect(mock).toHaveBeenCalledTimes(4);
56
+ expect(mock).toHaveBeenCalledWith({
57
+ entity,
58
+ component: Position,
59
+ value: [undefined, { x: 3, y: 3 }],
60
+ type: UpdateType.Exit,
61
+ });
62
+ });
63
+
64
+ it("defineUpdateSystem should rerun the system if the component value of an entity matchign the query changes", () => {
65
+ const mock = jest.fn();
66
+ defineUpdateSystem(world, [Has(Position)], mock);
67
+
68
+ // The entity already had a position when the system was created and the system runs on init,
69
+ // so this position update is an update
70
+ setComponent(Position, entity, { x: 2, y: 3 });
71
+ expect(mock).toHaveBeenCalledTimes(1);
72
+
73
+ setComponent(Position, entity, { x: 2, y: 3 });
74
+ expect(mock).toHaveBeenCalledTimes(2);
75
+ expect(mock).toHaveBeenCalledWith({
76
+ entity,
77
+ component: Position,
78
+ value: [
79
+ { x: 2, y: 3 },
80
+ { x: 2, y: 3 },
81
+ ],
82
+ type: UpdateType.Update,
83
+ });
84
+
85
+ // Setting the same value again should rerun the system
86
+ setComponent(Position, entity, { x: 2, y: 3 });
87
+ expect(mock).toHaveBeenCalledTimes(3);
88
+
89
+ setComponent(Position, entity, { x: 3, y: 3 });
90
+ expect(mock).toHaveBeenCalledTimes(4);
91
+ });
92
+
93
+ it("defineEnterSystem should rerun once with entities matching the query for the first time", () => {
94
+ const CanMove = defineComponent(world, { value: Type.Boolean });
95
+ const mock = jest.fn();
96
+
97
+ defineEnterSystem(world, [Has(CanMove)], mock);
98
+
99
+ const entity1 = createEntity(world, [withValue(CanMove, { value: true })]);
100
+
101
+ expect(mock).toHaveBeenCalledTimes(1);
102
+ expect(mock).toHaveBeenCalledWith(
103
+ expect.objectContaining({ entity: entity1, component: CanMove, value: [{ value: true }, undefined] })
104
+ );
105
+
106
+ const entity2 = createEntity(world, [withValue(CanMove, { value: true })]);
107
+ expect(mock).toHaveBeenCalledTimes(2);
108
+ expect(mock).toHaveBeenCalledWith(
109
+ expect.objectContaining({ entity: entity2, component: CanMove, value: [{ value: true }, undefined] })
110
+ );
111
+ });
112
+
113
+ it("defineExitSystem should rerun once with entities not matching the query anymore", () => {
114
+ const CanMove = defineComponent(world, { value: Type.Boolean });
115
+
116
+ const mock = jest.fn();
117
+ defineExitSystem(world, [Has(CanMove)], mock);
118
+
119
+ const entity1 = createEntity(world, [withValue(CanMove, { value: true })]);
120
+ const entity2 = createEntity(world);
121
+ setComponent(CanMove, entity2, { value: true });
122
+
123
+ expect(mock).toHaveBeenCalledTimes(0);
124
+
125
+ removeComponent(CanMove, entity1);
126
+
127
+ expect(mock).toHaveBeenCalledTimes(1);
128
+ expect(mock).toHaveBeenCalledWith(
129
+ expect.objectContaining({ entity: entity1, component: CanMove, value: [undefined, { value: true }] })
130
+ );
131
+
132
+ removeComponent(CanMove, entity2);
133
+ expect(mock).toHaveBeenCalledTimes(2);
134
+ expect(mock).toHaveBeenCalledWith(
135
+ expect.objectContaining({ entity: entity2, component: CanMove, value: [undefined, { value: true }] })
136
+ );
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,79 @@
1
+ import { arrayToIterator } from "@latticexyz/utils";
2
+ import { Subject } from "rxjs";
3
+ import { defineComponent, setComponent } from "./Component";
4
+ import { Type } from "./constants";
5
+ import { createEntity } from "./Entity";
6
+ import { World, AnyComponent, EntitySymbol } from "./types";
7
+ import { createWorld, getEntityComponents } from "./World";
8
+
9
+ describe("World", () => {
10
+ describe("createWorld", () => {
11
+ let world: World;
12
+
13
+ beforeEach(() => {
14
+ world = createWorld();
15
+ });
16
+
17
+ describe("registerEntity", () => {
18
+ it("should add the entitiy to the world's entities", () => {
19
+ expect([...world.getEntities()].length).toBe(0);
20
+
21
+ world.registerEntity();
22
+ expect([...world.getEntities()].length).toBe(1);
23
+ expect([...world.getEntities()][0]).not.toBe(undefined);
24
+ });
25
+ });
26
+
27
+ describe("registerComponent", () => {
28
+ let rawComponent: AnyComponent;
29
+
30
+ beforeEach(() => {
31
+ rawComponent = {
32
+ id: "some-id",
33
+ values: {
34
+ value: new Map<EntitySymbol, number>(),
35
+ },
36
+ update$: new Subject(),
37
+ schema: { value: Type.Number },
38
+ metadata: {},
39
+ entities: () => arrayToIterator([]),
40
+ world,
41
+ };
42
+ });
43
+
44
+ it("should add the component to the world's components", () => {
45
+ expect(world.components.length).toBe(0);
46
+ world.registerComponent(rawComponent);
47
+ expect(world.components.length).toBe(1);
48
+ expect(world.components.includes(rawComponent)).toBe(true);
49
+ });
50
+
51
+ describe("getEntitiyComponents", () => {
52
+ it("should return the set of components of a given entity", () => {
53
+ const components = new Set<AnyComponent>([
54
+ defineComponent(world, { value: Type.Number }),
55
+ defineComponent(world, { value: Type.Number }),
56
+ defineComponent(world, { value: Type.Number }),
57
+ ]);
58
+ const entity = createEntity(world);
59
+ const value = 1;
60
+
61
+ for (const component of components) {
62
+ setComponent(component, entity, { value });
63
+ }
64
+
65
+ const received = getEntityComponents(world, entity);
66
+ expect(received.length).toEqual(components.size);
67
+
68
+ for (const comp of received) {
69
+ expect(components.has(comp)).toBe(true);
70
+ }
71
+
72
+ for (const comp of components) {
73
+ expect(received.includes(comp)).toBe(true);
74
+ }
75
+ });
76
+ });
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,9 @@
1
+ export enum ActionState {
2
+ Requested = "Requested",
3
+ Executing = "Executing",
4
+ WaitingForTxEvents = "WaitingForTxEvents",
5
+ Complete = "Complete",
6
+ Failed = "Failed",
7
+ Cancelled = "Cancelled",
8
+ TxReduced = "TxReduced",
9
+ }