@inglorious/store 8.0.0 → 8.0.1
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 +6 -4
- package/types/client/devtools.d.ts +10 -0
- package/types/index.d.ts +3 -0
- package/types/select.d.ts +24 -0
- package/types/store.d.ts +145 -0
- package/types/test.d.ts +47 -0
- package/src/entities.test.js +0 -51
- package/src/event-map.test.js +0 -214
- package/src/store.test.js +0 -178
- package/src/test.test.js +0 -272
- package/src/types.test.js +0 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,17 +42,19 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
|
-
"src"
|
|
45
|
+
"src",
|
|
46
|
+
"!src/**/*.test.js",
|
|
47
|
+
"types"
|
|
46
48
|
],
|
|
47
49
|
"publishConfig": {
|
|
48
50
|
"access": "public"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"mutative": "^1.3.0",
|
|
52
|
-
"@inglorious/utils": "3.7.
|
|
54
|
+
"@inglorious/utils": "3.7.1"
|
|
53
55
|
},
|
|
54
56
|
"peerDependencies": {
|
|
55
|
-
"@inglorious/utils": "3.7.
|
|
57
|
+
"@inglorious/utils": "3.7.1"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"prettier": "^3.6.2",
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input selector function that takes state and returns a value
|
|
3
|
+
*/
|
|
4
|
+
export type InputSelector<TState = any, TResult = any> = (
|
|
5
|
+
state: TState,
|
|
6
|
+
) => TResult
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Output selector function (memoized)
|
|
10
|
+
*/
|
|
11
|
+
export type OutputSelector<TState = any, TResult = any> = (
|
|
12
|
+
state: TState,
|
|
13
|
+
) => TResult
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a memoized selector function.
|
|
17
|
+
* @param inputSelectors - An array of input selector functions.
|
|
18
|
+
* @param resultFunc - A function that receives the results of the input selectors and returns a computed value.
|
|
19
|
+
* @returns A memoized selector function that, when called, returns the selected state.
|
|
20
|
+
*/
|
|
21
|
+
export function createSelector<TState = any, TResult = any>(
|
|
22
|
+
inputSelectors: InputSelector<TState, any>[],
|
|
23
|
+
resultFunc: (...args: any[]) => TResult,
|
|
24
|
+
): OutputSelector<TState, TResult>
|
package/types/store.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base entity structure
|
|
3
|
+
*/
|
|
4
|
+
export interface BaseEntity {
|
|
5
|
+
type: string
|
|
6
|
+
[key: string]: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Event structure
|
|
11
|
+
*/
|
|
12
|
+
export interface Event<T = any> {
|
|
13
|
+
type: string
|
|
14
|
+
payload?: T
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Entity type definition with event handlers
|
|
19
|
+
*/
|
|
20
|
+
export type EntityType<TEntity extends BaseEntity = BaseEntity> = {
|
|
21
|
+
[K: string]: (entity: TEntity, payload: any, api: Api) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* System definition with event handlers
|
|
26
|
+
*/
|
|
27
|
+
export type System<TState extends EntitiesState = EntitiesState> = {
|
|
28
|
+
[K: string]: (state: TState, payload: any, api: Api) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* State structure (entities indexed by ID)
|
|
33
|
+
*/
|
|
34
|
+
export type EntitiesState<TEntity extends BaseEntity = BaseEntity> = {
|
|
35
|
+
[id: string]: TEntity
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Types configuration
|
|
40
|
+
*/
|
|
41
|
+
export type TypesConfig<TEntity extends BaseEntity = BaseEntity> = {
|
|
42
|
+
[typeName: string]: EntityType<TEntity>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Store configuration
|
|
47
|
+
*/
|
|
48
|
+
export interface StoreConfig<
|
|
49
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
50
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
51
|
+
> {
|
|
52
|
+
types?: TypesConfig<TEntity>
|
|
53
|
+
entities?: TState
|
|
54
|
+
systems?: System<TState>[]
|
|
55
|
+
middlewares?: Middleware<TEntity, TState>[]
|
|
56
|
+
mode?: "eager" | "batched"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Listener function for state updates
|
|
61
|
+
*/
|
|
62
|
+
export type Listener = () => void
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Unsubscribe function
|
|
66
|
+
*/
|
|
67
|
+
export type Unsubscribe = () => void
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* API object exposed to handlers
|
|
71
|
+
*/
|
|
72
|
+
export interface Api<
|
|
73
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
74
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
75
|
+
> {
|
|
76
|
+
getTypes: () => TypesConfig<TEntity>
|
|
77
|
+
getType: (typeName: string) => EntityType<TEntity>
|
|
78
|
+
getEntities: () => TState
|
|
79
|
+
getEntity: (id: string) => TEntity | undefined
|
|
80
|
+
dispatch: (event: Event) => void
|
|
81
|
+
notify: (type: string, payload?: any) => void
|
|
82
|
+
[key: string]: any // For middleware extras
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Base store interface
|
|
87
|
+
*/
|
|
88
|
+
export interface Store<
|
|
89
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
90
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
91
|
+
> {
|
|
92
|
+
subscribe: (listener: Listener) => Unsubscribe
|
|
93
|
+
update: () => Event[]
|
|
94
|
+
notify: (type: string, payload?: any) => void
|
|
95
|
+
dispatch: (event: Event) => void
|
|
96
|
+
getTypes: () => TypesConfig<TEntity>
|
|
97
|
+
getType: (typeName: string) => EntityType<TEntity>
|
|
98
|
+
getState: () => TState
|
|
99
|
+
setState: (nextState: TState) => void
|
|
100
|
+
reset: () => void
|
|
101
|
+
_api?: Api<TEntity, TState>
|
|
102
|
+
extras?: Record<string, any>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Middleware function type
|
|
107
|
+
*/
|
|
108
|
+
export type Middleware<
|
|
109
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
110
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
111
|
+
> = (store: Store<TEntity, TState>) => Store<TEntity, TState>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Built-in event payloads
|
|
115
|
+
*/
|
|
116
|
+
export interface MorphEventPayload {
|
|
117
|
+
id: string
|
|
118
|
+
type: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type AddEventPayload<TEntity extends BaseEntity = BaseEntity> =
|
|
122
|
+
TEntity & {
|
|
123
|
+
id: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type RemoveEventPayload = string
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a store to manage state and events
|
|
130
|
+
*/
|
|
131
|
+
export function createStore<
|
|
132
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
133
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
134
|
+
>(config: StoreConfig<TEntity, TState>): Store<TEntity, TState>
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates an API object
|
|
138
|
+
*/
|
|
139
|
+
export function createApi<
|
|
140
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
141
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
142
|
+
>(
|
|
143
|
+
store: Store<TEntity, TState>,
|
|
144
|
+
extras?: Record<string, any>,
|
|
145
|
+
): Api<TEntity, TState>
|
package/types/test.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock API for testing (subset of full API)
|
|
3
|
+
*/
|
|
4
|
+
export interface MockApi<
|
|
5
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
6
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
7
|
+
> {
|
|
8
|
+
getEntities: () => TState
|
|
9
|
+
getEntity: (id: string) => TEntity | undefined
|
|
10
|
+
dispatch: (event: Event) => void
|
|
11
|
+
notify: (type: string, payload?: any) => void
|
|
12
|
+
getEvents: () => Event[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of trigger function
|
|
17
|
+
*/
|
|
18
|
+
export interface TriggerResult<TState extends EntitiesState = EntitiesState> {
|
|
19
|
+
entities: TState
|
|
20
|
+
events: Event[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a mock API for testing event handlers
|
|
25
|
+
*/
|
|
26
|
+
export function createMockApi<
|
|
27
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
28
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
29
|
+
>(entities: TState): MockApi<TEntity, TState>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Triggers an event handler in isolation for testing
|
|
33
|
+
*/
|
|
34
|
+
export function trigger<
|
|
35
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
36
|
+
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
37
|
+
TPayload = any,
|
|
38
|
+
>(
|
|
39
|
+
entities: TState,
|
|
40
|
+
eventHandler: (
|
|
41
|
+
state: TState,
|
|
42
|
+
payload: TPayload,
|
|
43
|
+
api: MockApi<TEntity, TState>,
|
|
44
|
+
) => void,
|
|
45
|
+
eventPayload: TPayload,
|
|
46
|
+
api?: MockApi<TEntity, TState>,
|
|
47
|
+
): TriggerResult<TState>
|
package/src/entities.test.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { augmentEntities, augmentEntity } from "./entities.js"
|
|
4
|
-
|
|
5
|
-
test("augmentEntity should add an id to an entity", () => {
|
|
6
|
-
const entity = { type: "player", health: 100 }
|
|
7
|
-
const result = augmentEntity("player1", entity)
|
|
8
|
-
|
|
9
|
-
expect(result).toStrictEqual({ id: "player1", type: "player", health: 100 })
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test("augmentEntity should not mutate the original entity", () => {
|
|
13
|
-
const entity = { type: "player" }
|
|
14
|
-
augmentEntity("player1", entity)
|
|
15
|
-
|
|
16
|
-
expect(entity).toStrictEqual({ type: "player" })
|
|
17
|
-
expect(entity).not.toHaveProperty("id")
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test("augmentEntity should overwrite an existing id property", () => {
|
|
21
|
-
const entity = { id: "oldId", type: "player" }
|
|
22
|
-
const result = augmentEntity("newId", entity)
|
|
23
|
-
|
|
24
|
-
expect(result).toStrictEqual({ id: "newId", type: "player" })
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test("augmentEntities should augment a collection of entities", () => {
|
|
28
|
-
const entities = {
|
|
29
|
-
player1: { type: "player", health: 100 },
|
|
30
|
-
enemy1: { type: "enemy", damage: 10 },
|
|
31
|
-
}
|
|
32
|
-
const result = augmentEntities(entities)
|
|
33
|
-
|
|
34
|
-
expect(result).toStrictEqual({
|
|
35
|
-
player1: { id: "player1", type: "player", health: 100 },
|
|
36
|
-
enemy1: { id: "enemy1", type: "enemy", damage: 10 },
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test("augmentEntities should handle an empty collection", () => {
|
|
41
|
-
const result = augmentEntities({})
|
|
42
|
-
|
|
43
|
-
expect(result).toStrictEqual({})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("augmentEntities should not mutate the original entities object", () => {
|
|
47
|
-
const entities = { player1: { type: "player" } }
|
|
48
|
-
augmentEntities(entities)
|
|
49
|
-
|
|
50
|
-
expect(entities.player1).not.toHaveProperty("id")
|
|
51
|
-
})
|
package/src/event-map.test.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks"
|
|
2
|
-
|
|
3
|
-
import { expect, test, vi } from "vitest"
|
|
4
|
-
|
|
5
|
-
import { EventMap } from "./event-map.js"
|
|
6
|
-
|
|
7
|
-
test("constructor should initialize the event map from types and entities", () => {
|
|
8
|
-
const types = {
|
|
9
|
-
player: {
|
|
10
|
-
update: () => {},
|
|
11
|
-
fire: () => {},
|
|
12
|
-
},
|
|
13
|
-
enemy: {
|
|
14
|
-
update: () => {},
|
|
15
|
-
},
|
|
16
|
-
item: {}, // Type with no events
|
|
17
|
-
}
|
|
18
|
-
const entities = {
|
|
19
|
-
player1: { type: "player" },
|
|
20
|
-
player2: { type: "player" },
|
|
21
|
-
enemy1: { type: "enemy" },
|
|
22
|
-
item1: { type: "item" },
|
|
23
|
-
ghost1: { type: "ghost" }, // Type that doesn't exist
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const eventMap = new EventMap(types, entities)
|
|
27
|
-
|
|
28
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual([
|
|
29
|
-
"player1",
|
|
30
|
-
"player2",
|
|
31
|
-
"enemy1",
|
|
32
|
-
])
|
|
33
|
-
expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual([
|
|
34
|
-
"player1",
|
|
35
|
-
"player2",
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
// 'item' type has no events, so it shouldn't be in the map
|
|
39
|
-
expect(eventMap.getEntitiesForEvent("item")).toStrictEqual([])
|
|
40
|
-
// 'ghost' type doesn't exist, so it should be ignored
|
|
41
|
-
expect(eventMap.getEntitiesForEvent("ghost")).toStrictEqual([])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test("addEntity should add an entity to the correct event sets", () => {
|
|
45
|
-
const types = {
|
|
46
|
-
player: {
|
|
47
|
-
update: () => {},
|
|
48
|
-
jump: () => {},
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
const eventMap = new EventMap(types, {})
|
|
52
|
-
|
|
53
|
-
eventMap.addEntity("player1", types.player, "player")
|
|
54
|
-
|
|
55
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player1"])
|
|
56
|
-
expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(["player1"])
|
|
57
|
-
|
|
58
|
-
// Add another entity of the same type
|
|
59
|
-
eventMap.addEntity("player2", types.player, "player")
|
|
60
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual([
|
|
61
|
-
"player1",
|
|
62
|
-
"player2",
|
|
63
|
-
])
|
|
64
|
-
expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual([
|
|
65
|
-
"player1",
|
|
66
|
-
"player2",
|
|
67
|
-
])
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test("removeEntity should remove an entity from its event sets", () => {
|
|
71
|
-
const types = {
|
|
72
|
-
player: {
|
|
73
|
-
update: () => {},
|
|
74
|
-
fire: () => {},
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
const entities = {
|
|
78
|
-
player1: { type: "player" },
|
|
79
|
-
player2: { type: "player" },
|
|
80
|
-
}
|
|
81
|
-
const eventMap = new EventMap(types, entities)
|
|
82
|
-
|
|
83
|
-
eventMap.removeEntity("player1", types.player, "player")
|
|
84
|
-
|
|
85
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player2"])
|
|
86
|
-
expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(["player2"])
|
|
87
|
-
|
|
88
|
-
// Removing a non-existent entity should not throw an error
|
|
89
|
-
expect(() =>
|
|
90
|
-
eventMap.removeEntity("player3", types.player, "player"),
|
|
91
|
-
).not.toThrow()
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test("getEntitiesForEvent should return the correct set of entities for an event", () => {
|
|
95
|
-
const types = {
|
|
96
|
-
player: { update: () => {} },
|
|
97
|
-
enemy: { update: () => {} },
|
|
98
|
-
}
|
|
99
|
-
const entities = {
|
|
100
|
-
player1: { type: "player" },
|
|
101
|
-
enemy1: { type: "enemy" },
|
|
102
|
-
}
|
|
103
|
-
const eventMap = new EventMap(types, entities)
|
|
104
|
-
|
|
105
|
-
const updateEntities = eventMap.getEntitiesForEvent("update")
|
|
106
|
-
expect(updateEntities).toStrictEqual(["player1", "enemy1"])
|
|
107
|
-
|
|
108
|
-
const fireEntities = eventMap.getEntitiesForEvent("fire")
|
|
109
|
-
expect(fireEntities).toStrictEqual([])
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test("getEntitiesForEvent should handle scoped events correctly", () => {
|
|
113
|
-
const types = {
|
|
114
|
-
form: { submit: () => {} },
|
|
115
|
-
button: { submit: () => {} },
|
|
116
|
-
}
|
|
117
|
-
const entities = {
|
|
118
|
-
loginForm: { type: "form" },
|
|
119
|
-
signupForm: { type: "form" },
|
|
120
|
-
submitButton: { type: "button" },
|
|
121
|
-
}
|
|
122
|
-
const eventMap = new EventMap(types, entities)
|
|
123
|
-
|
|
124
|
-
// Scoped to a type: 'form:submit' should only return form entities
|
|
125
|
-
expect(eventMap.getEntitiesForEvent("form:submit")).toStrictEqual([
|
|
126
|
-
"loginForm",
|
|
127
|
-
"signupForm",
|
|
128
|
-
])
|
|
129
|
-
|
|
130
|
-
// Scoped to a specific entity by ID: '#loginForm:submit'
|
|
131
|
-
expect(eventMap.getEntitiesForEvent("#loginForm:submit")).toStrictEqual([
|
|
132
|
-
"loginForm",
|
|
133
|
-
])
|
|
134
|
-
|
|
135
|
-
// Scoped to a specific entity by type and ID: 'form#loginForm:submit'
|
|
136
|
-
expect(eventMap.getEntitiesForEvent("form#loginForm:submit")).toStrictEqual([
|
|
137
|
-
"loginForm",
|
|
138
|
-
])
|
|
139
|
-
|
|
140
|
-
// Scoped to a non-existent entity ID
|
|
141
|
-
expect(eventMap.getEntitiesForEvent("#nonExistent:submit")).toStrictEqual([])
|
|
142
|
-
|
|
143
|
-
// Scoped to an entity that exists but doesn't handle the event
|
|
144
|
-
expect(eventMap.getEntitiesForEvent("#loginForm:click")).toStrictEqual([])
|
|
145
|
-
|
|
146
|
-
// Scoped to an entity ID, but with the wrong type specified
|
|
147
|
-
expect(eventMap.getEntitiesForEvent("button#loginForm:submit")).toStrictEqual(
|
|
148
|
-
[],
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
// Broadcast event should return all entities with the handler
|
|
152
|
-
expect(eventMap.getEntitiesForEvent("submit")).toStrictEqual([
|
|
153
|
-
"loginForm",
|
|
154
|
-
"signupForm",
|
|
155
|
-
"submitButton",
|
|
156
|
-
])
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test("EventMap provides a significant performance benefit for event handling", async () => {
|
|
160
|
-
const ENTITY_COUNT = 10000
|
|
161
|
-
const { entities, types } = createTestEntities(ENTITY_COUNT)
|
|
162
|
-
const eventMap = new EventMap(types, entities)
|
|
163
|
-
|
|
164
|
-
// We'll use a mock function to ensure the "work" is consistent
|
|
165
|
-
const updateHandler = vi.fn()
|
|
166
|
-
types.updater.update = updateHandler
|
|
167
|
-
|
|
168
|
-
// --- Simulation A: The Old Way (iterating all entities) ---
|
|
169
|
-
const oldWayStartTime = performance.now()
|
|
170
|
-
for (const id in entities) {
|
|
171
|
-
const entity = entities[id]
|
|
172
|
-
const type = types[entity.type]
|
|
173
|
-
if (type.update) {
|
|
174
|
-
type.update()
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const oldWayTime = performance.now() - oldWayStartTime
|
|
178
|
-
|
|
179
|
-
// Reset the mock for the next simulation
|
|
180
|
-
updateHandler.mockClear()
|
|
181
|
-
|
|
182
|
-
// --- Simulation B: The New Way (using EventMap) ---
|
|
183
|
-
const newWayStartTime = performance.now()
|
|
184
|
-
const updaterIds = eventMap.getEntitiesForEvent("update")
|
|
185
|
-
for (const id of updaterIds) {
|
|
186
|
-
const entity = entities[id]
|
|
187
|
-
const type = types[entity.type]
|
|
188
|
-
type.update()
|
|
189
|
-
}
|
|
190
|
-
const newWayTime = performance.now() - newWayStartTime
|
|
191
|
-
|
|
192
|
-
// Assertions to verify correctness
|
|
193
|
-
expect(oldWayTime).toBeGreaterThan(newWayTime)
|
|
194
|
-
expect(updateHandler).toHaveBeenCalledTimes(updaterIds.length)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Helper function to create a large set of test entities
|
|
198
|
-
function createTestEntities(count) {
|
|
199
|
-
const entities = {}
|
|
200
|
-
const types = {}
|
|
201
|
-
// 10% of entities will have a mock 'update' handler
|
|
202
|
-
const updaterType = { update: vi.fn() }
|
|
203
|
-
const staticType = {}
|
|
204
|
-
|
|
205
|
-
for (let i = 0; i < count; i++) {
|
|
206
|
-
const isUpdater = Math.random() < 0.1
|
|
207
|
-
const typeId = isUpdater ? "updater" : "static"
|
|
208
|
-
entities[`entity-${i}`] = { id: `entity-${i}`, type: typeId }
|
|
209
|
-
}
|
|
210
|
-
types["updater"] = updaterType
|
|
211
|
-
types["static"] = staticType
|
|
212
|
-
|
|
213
|
-
return { entities, types }
|
|
214
|
-
}
|
package/src/store.test.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { createStore } from "./store.js"
|
|
4
|
-
|
|
5
|
-
test("it should process events by mutating state inside handlers", () => {
|
|
6
|
-
const config = {
|
|
7
|
-
types: {
|
|
8
|
-
kitty: {
|
|
9
|
-
feed(entity) {
|
|
10
|
-
entity.isFed = true
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
entities: {
|
|
15
|
-
kitty1: { type: "kitty" },
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
const afterState = {
|
|
19
|
-
kitty1: {
|
|
20
|
-
id: "kitty1",
|
|
21
|
-
type: "kitty",
|
|
22
|
-
isFed: true,
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const store = createStore(config)
|
|
27
|
-
store.notify("feed")
|
|
28
|
-
store.update()
|
|
29
|
-
|
|
30
|
-
const state = store.getState()
|
|
31
|
-
expect(state).toStrictEqual(afterState)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test("it should process an event queue in the same update cycle", () => {
|
|
35
|
-
const config = {
|
|
36
|
-
types: {
|
|
37
|
-
kitty: {
|
|
38
|
-
feed(entity) {
|
|
39
|
-
entity.isFed = true
|
|
40
|
-
},
|
|
41
|
-
update(entity) {
|
|
42
|
-
entity.isMeowing = true
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
entities: {
|
|
48
|
-
kitty1: { type: "kitty" },
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
const afterState = {
|
|
52
|
-
kitty1: {
|
|
53
|
-
id: "kitty1",
|
|
54
|
-
type: "kitty",
|
|
55
|
-
isFed: true,
|
|
56
|
-
isMeowing: true,
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const store = createStore(config)
|
|
61
|
-
store.notify("feed")
|
|
62
|
-
store.notify("update")
|
|
63
|
-
store.update()
|
|
64
|
-
|
|
65
|
-
const state = store.getState()
|
|
66
|
-
expect(state).toStrictEqual(afterState)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("it should send an event from an entity and process it in the same update cycle in batched mode", () => {
|
|
70
|
-
const config = {
|
|
71
|
-
types: {
|
|
72
|
-
doggo: {
|
|
73
|
-
update(entity, dt, api) {
|
|
74
|
-
api.notify("bark")
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
kitty: {
|
|
78
|
-
bark(entity) {
|
|
79
|
-
entity.position = "far"
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
entities: {
|
|
85
|
-
doggo1: { type: "doggo" },
|
|
86
|
-
kitty1: { type: "kitty", position: "near" },
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
updateMode: "manual",
|
|
90
|
-
}
|
|
91
|
-
const afterState = {
|
|
92
|
-
doggo1: { id: "doggo1", type: "doggo" },
|
|
93
|
-
kitty1: { id: "kitty1", type: "kitty", position: "far" },
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const store = createStore(config)
|
|
97
|
-
const api = { notify: store.notify }
|
|
98
|
-
store.notify("update")
|
|
99
|
-
store.update(api)
|
|
100
|
-
|
|
101
|
-
const state = store.getState()
|
|
102
|
-
expect(state).toStrictEqual(afterState)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test("it should add an entity via an 'add' event", () => {
|
|
106
|
-
const config = {
|
|
107
|
-
types: {
|
|
108
|
-
kitty: {},
|
|
109
|
-
},
|
|
110
|
-
entities: {},
|
|
111
|
-
}
|
|
112
|
-
const newEntity = { id: "kitty1", type: "kitty" }
|
|
113
|
-
const afterState = {
|
|
114
|
-
kitty1: { id: "kitty1", type: "kitty" },
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const store = createStore(config)
|
|
118
|
-
store.notify("add", newEntity)
|
|
119
|
-
store.update()
|
|
120
|
-
|
|
121
|
-
const state = store.getState()
|
|
122
|
-
expect(state).toStrictEqual(afterState)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test("it should remove an entity via a 'remove' event", () => {
|
|
126
|
-
const config = {
|
|
127
|
-
types: {},
|
|
128
|
-
entities: {
|
|
129
|
-
kitty1: { type: "kitty" },
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
const store = createStore(config)
|
|
133
|
-
|
|
134
|
-
store.notify("remove", "kitty1")
|
|
135
|
-
store.update()
|
|
136
|
-
|
|
137
|
-
const state = store.getState()
|
|
138
|
-
expect(state.kitty1).toBeUndefined()
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
test("it should change an entity's behavior via a 'morph' event", () => {
|
|
142
|
-
const Caterpillar = {
|
|
143
|
-
eat(entity) {
|
|
144
|
-
entity.isFull = true
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
const Butterfly = {
|
|
148
|
-
fly(entity) {
|
|
149
|
-
entity.hasFlown = true
|
|
150
|
-
},
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const config = {
|
|
154
|
-
types: {
|
|
155
|
-
bug: Caterpillar,
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
entities: {
|
|
159
|
-
bug: { type: "bug" },
|
|
160
|
-
},
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const store = createStore(config)
|
|
164
|
-
|
|
165
|
-
store.notify("eat")
|
|
166
|
-
store.update()
|
|
167
|
-
|
|
168
|
-
expect(store.getState()).toStrictEqual({
|
|
169
|
-
bug: { id: "bug", type: "bug", isFull: true },
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
store.notify("morph", { name: "bug", type: [Caterpillar, Butterfly] })
|
|
173
|
-
store.notify("fly")
|
|
174
|
-
store.update()
|
|
175
|
-
expect(store.getState()).toStrictEqual({
|
|
176
|
-
bug: { id: "bug", type: "bug", isFull: true, hasFlown: true },
|
|
177
|
-
})
|
|
178
|
-
})
|
package/src/test.test.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { createMockApi, trigger } from "./test"
|
|
4
|
-
|
|
5
|
-
describe("createMockApi", () => {
|
|
6
|
-
it("should create a mock API with all required methods", () => {
|
|
7
|
-
const entities = { counter1: { type: "counter", value: 0 } }
|
|
8
|
-
|
|
9
|
-
const api = createMockApi(entities)
|
|
10
|
-
|
|
11
|
-
expect(api.getEntities).toBeDefined()
|
|
12
|
-
expect(api.getEntity).toBeDefined()
|
|
13
|
-
expect(api.dispatch).toBeDefined()
|
|
14
|
-
expect(api.notify).toBeDefined()
|
|
15
|
-
expect(api.getEvents).toBeDefined()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("should return all entities via getEntities()", () => {
|
|
19
|
-
const entities = {
|
|
20
|
-
counter1: { type: "counter", value: 5 },
|
|
21
|
-
counter2: { type: "counter", value: 10 },
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const api = createMockApi(entities)
|
|
25
|
-
|
|
26
|
-
expect(api.getEntities()).toEqual(entities)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it("should return a specific entity via getEntity()", () => {
|
|
30
|
-
const entities = {
|
|
31
|
-
counter1: { type: "counter", value: 5 },
|
|
32
|
-
counter2: { type: "counter", value: 10 },
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const api = createMockApi(entities)
|
|
36
|
-
|
|
37
|
-
expect(api.getEntity("counter1")).toEqual({ type: "counter", value: 5 })
|
|
38
|
-
expect(api.getEntity("counter2")).toEqual({ type: "counter", value: 10 })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("should return undefined for non-existent entity", () => {
|
|
42
|
-
const entities = {
|
|
43
|
-
counter1: { type: "counter", value: 5 },
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const api = createMockApi(entities)
|
|
47
|
-
|
|
48
|
-
expect(api.getEntity("nonexistent")).toBeUndefined()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it("should track dispatched events", () => {
|
|
52
|
-
const entities = {
|
|
53
|
-
counter1: { type: "counter", value: 0 },
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const api = createMockApi(entities)
|
|
57
|
-
|
|
58
|
-
api.dispatch({ type: "increment", payload: { id: "counter1" } })
|
|
59
|
-
api.dispatch({ type: "decrement", payload: { id: "counter1" } })
|
|
60
|
-
|
|
61
|
-
expect(api.getEvents()).toEqual([
|
|
62
|
-
{ type: "increment", payload: { id: "counter1" } },
|
|
63
|
-
{ type: "decrement", payload: { id: "counter1" } },
|
|
64
|
-
])
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it("should track events dispatched via notify()", () => {
|
|
68
|
-
const entities = {
|
|
69
|
-
counter1: { type: "counter", value: 0 },
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const api = createMockApi(entities)
|
|
73
|
-
|
|
74
|
-
api.notify("increment", { id: "counter1", amount: 5 })
|
|
75
|
-
api.notify("overflow")
|
|
76
|
-
|
|
77
|
-
expect(api.getEvents()).toEqual([
|
|
78
|
-
{ type: "increment", payload: { id: "counter1", amount: 5 } },
|
|
79
|
-
{ type: "overflow", payload: undefined },
|
|
80
|
-
])
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it("should freeze entities to prevent mutations", () => {
|
|
84
|
-
const entities = {
|
|
85
|
-
counter1: { type: "counter", value: 0 },
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const api = createMockApi(entities)
|
|
89
|
-
const allEntities = api.getEntities()
|
|
90
|
-
|
|
91
|
-
expect(() => {
|
|
92
|
-
allEntities.counter1 = { type: "counter", value: 999 }
|
|
93
|
-
}).toThrow()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it("should start with empty events array", () => {
|
|
97
|
-
const entities = {
|
|
98
|
-
counter1: { type: "counter", value: 0 },
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const api = createMockApi(entities)
|
|
102
|
-
|
|
103
|
-
expect(api.getEvents()).toEqual([])
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe("trigger", () => {
|
|
108
|
-
it("should execute handler and return new entity", () => {
|
|
109
|
-
const entityBefore = { type: "counter", value: 0 }
|
|
110
|
-
const entityAfter = { type: "counter", value: 5 }
|
|
111
|
-
|
|
112
|
-
function increment(entity, amount) {
|
|
113
|
-
entity.value += amount
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { entity } = trigger(entityBefore, increment, 5)
|
|
117
|
-
|
|
118
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it("should not mutate original entity", () => {
|
|
122
|
-
const entityBefore = { type: "counter", value: 0 }
|
|
123
|
-
|
|
124
|
-
function increment(entity, amount) {
|
|
125
|
-
entity.value += amount
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
trigger(entityBefore, increment, 5)
|
|
129
|
-
|
|
130
|
-
expect(entityBefore.value).toBe(0)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it("should capture events dispatched during handler execution", () => {
|
|
134
|
-
const entityBefore = { type: "counter", value: 99 }
|
|
135
|
-
const entityAfter = { type: "counter", value: 104 }
|
|
136
|
-
const eventsAfter = [{ type: "overflow", payload: 104 }]
|
|
137
|
-
|
|
138
|
-
function increment(entity, amount, api) {
|
|
139
|
-
entity.value += amount
|
|
140
|
-
|
|
141
|
-
if (entity.value > 100) {
|
|
142
|
-
api.notify("overflow", entity.value)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const { entity, events } = trigger(entityBefore, increment, 5)
|
|
147
|
-
|
|
148
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
149
|
-
expect(events).toStrictEqual(eventsAfter)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it("should work with handlers that do not dispatch events", () => {
|
|
153
|
-
const entityBefore = { type: "todo", text: "Buy milk", completed: false }
|
|
154
|
-
const entityAfter = { type: "todo", text: "Buy milk", completed: true }
|
|
155
|
-
const eventsAfter = []
|
|
156
|
-
|
|
157
|
-
function toggle(entity) {
|
|
158
|
-
entity.completed = !entity.completed
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const { entity, events } = trigger(entityBefore, toggle)
|
|
162
|
-
|
|
163
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
164
|
-
expect(events).toStrictEqual(eventsAfter)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it("should allow handler to read other entities via API", () => {
|
|
168
|
-
const entityBefore = { id: "counter2", type: "counter", value: 20 }
|
|
169
|
-
const entityAfter = { id: "counter2", type: "counter", value: 30 }
|
|
170
|
-
|
|
171
|
-
const mockApi = createMockApi({
|
|
172
|
-
counter1: { id: "counter1", type: "counter", value: 10 },
|
|
173
|
-
counter2: { id: "counter2", type: "counter", value: 20 },
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
function addFromSource(entity, sourceId, api) {
|
|
177
|
-
const source = api.getEntity(sourceId)
|
|
178
|
-
if (source) {
|
|
179
|
-
entity.value += source.value
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const { entity } = trigger(entityBefore, addFromSource, "counter1", mockApi)
|
|
184
|
-
|
|
185
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it("should accept custom mock API", () => {
|
|
189
|
-
const entityBefore = { type: "counter", value: 0 }
|
|
190
|
-
const entityAfter = { type: "counter", value: 5 }
|
|
191
|
-
const eventsAfter = [{ type: "incremented", payload: undefined }]
|
|
192
|
-
|
|
193
|
-
const customApi = createMockApi(entityBefore)
|
|
194
|
-
|
|
195
|
-
function increment(entity, amount, api) {
|
|
196
|
-
entity.value += amount
|
|
197
|
-
api.notify("incremented")
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const { entity } = trigger(entityBefore, increment, 5, customApi)
|
|
201
|
-
|
|
202
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
203
|
-
expect(customApi.getEvents()).toStrictEqual(eventsAfter)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it("should handle multiple dispatched events", () => {
|
|
207
|
-
const entityBefore = { type: "counter", value: 50 }
|
|
208
|
-
const entityAfter = { type: "counter", value: 110 }
|
|
209
|
-
const eventsAfter = [
|
|
210
|
-
{ type: "increment:start", payload: undefined },
|
|
211
|
-
{ type: "milestone:hundred", payload: undefined },
|
|
212
|
-
{ type: "increment:complete", payload: 110 },
|
|
213
|
-
]
|
|
214
|
-
|
|
215
|
-
function increment(entity, amount, api) {
|
|
216
|
-
api.notify("increment:start")
|
|
217
|
-
|
|
218
|
-
entity.value += amount
|
|
219
|
-
|
|
220
|
-
if (entity.value >= 100) {
|
|
221
|
-
api.notify("milestone:hundred")
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
api.notify("increment:complete", entity.value)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const { entity, events } = trigger(entityBefore, increment, 60)
|
|
228
|
-
|
|
229
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
230
|
-
expect(events).toStrictEqual(eventsAfter)
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it("should work without payload", () => {
|
|
234
|
-
const entityBefore = { type: "counter", value: 5 }
|
|
235
|
-
const entityAfter = { type: "counter", value: 0 }
|
|
236
|
-
|
|
237
|
-
function reset(entity) {
|
|
238
|
-
entity.value = 0
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const { entity } = trigger(entityBefore, reset)
|
|
242
|
-
|
|
243
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it("should work with complex entity mutations", () => {
|
|
247
|
-
const entityBefore = {
|
|
248
|
-
type: "player",
|
|
249
|
-
name: "Alice",
|
|
250
|
-
inventory: ["sword", "shield"],
|
|
251
|
-
stats: { health: 100, mana: 50 },
|
|
252
|
-
}
|
|
253
|
-
const entityAfter = {
|
|
254
|
-
type: "player",
|
|
255
|
-
name: "Alice",
|
|
256
|
-
inventory: ["sword", "shield", "potion"],
|
|
257
|
-
stats: { health: 100, mana: 50 },
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function addItem(entity, item, api) {
|
|
261
|
-
entity.inventory.push(item)
|
|
262
|
-
api.notify("item:added", { item })
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const { entity, events } = trigger(entityBefore, addItem, "potion")
|
|
266
|
-
|
|
267
|
-
expect(entity).toStrictEqual(entityAfter)
|
|
268
|
-
expect(events).toEqual([
|
|
269
|
-
{ type: "item:added", payload: { item: "potion" } },
|
|
270
|
-
])
|
|
271
|
-
})
|
|
272
|
-
})
|
package/src/types.test.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { augmentType, augmentTypes } from "./types.js"
|
|
4
|
-
|
|
5
|
-
test("should handle a single behavior object (mixin)", () => {
|
|
6
|
-
const behavior = { onFire: () => "bang" }
|
|
7
|
-
const result = augmentType(behavior)
|
|
8
|
-
|
|
9
|
-
expect(result).toStrictEqual(behavior)
|
|
10
|
-
// Ensure it's a new object, not the same reference
|
|
11
|
-
expect(result).not.toBe(behavior)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test("should compose an array of behavior objects (mixins)", () => {
|
|
15
|
-
const behavior1 = { onFire: () => "bang", onMove: () => "vroom" }
|
|
16
|
-
const behavior2 = { onJump: () => "boing", onFire: () => "pow" } // onFire overwrites
|
|
17
|
-
const result = augmentType([behavior1, behavior2])
|
|
18
|
-
|
|
19
|
-
expect(result).toHaveProperty("onMove")
|
|
20
|
-
expect(result).toHaveProperty("onJump")
|
|
21
|
-
expect(result).toHaveProperty("onFire")
|
|
22
|
-
expect(result.onFire()).toBe("pow")
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test("should handle a single behavior function", () => {
|
|
26
|
-
const behavior = (type) => ({ ...type, onJump: () => "boing" })
|
|
27
|
-
const result = augmentType(behavior)
|
|
28
|
-
|
|
29
|
-
expect(result).toHaveProperty("onJump")
|
|
30
|
-
expect(result.onJump()).toBe("boing")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test("should compose an array of behavior functions (pipe)", () => {
|
|
34
|
-
const behavior1 = (type) => ({ ...type, onMove: () => "vroom" })
|
|
35
|
-
const behavior2 = (type) => ({ ...type, onJump: () => "boing" })
|
|
36
|
-
const result = augmentType([behavior1, behavior2])
|
|
37
|
-
|
|
38
|
-
expect(result).toHaveProperty("onMove")
|
|
39
|
-
expect(result).toHaveProperty("onJump")
|
|
40
|
-
expect(result.onMove()).toBe("vroom")
|
|
41
|
-
expect(result.onJump()).toBe("boing")
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test("should compose a mixed array of objects and functions", () => {
|
|
45
|
-
const mixin1 = { onFire: () => "bang" }
|
|
46
|
-
const behavior1 = (type) => ({ ...type, onJump: () => "boing" })
|
|
47
|
-
const mixin2 = { onMove: () => "vroom", onFire: () => "pow" } // onFire overwrites
|
|
48
|
-
|
|
49
|
-
const result = augmentType([mixin1, behavior1, mixin2])
|
|
50
|
-
|
|
51
|
-
expect(result).toHaveProperty("onFire")
|
|
52
|
-
expect(result).toHaveProperty("onJump")
|
|
53
|
-
expect(result).toHaveProperty("onMove")
|
|
54
|
-
expect(result.onFire()).toBe("pow")
|
|
55
|
-
expect(result.onJump()).toBe("boing")
|
|
56
|
-
expect(result.onMove()).toBe("vroom")
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test("should return an empty object for an empty array", () => {
|
|
60
|
-
const result = augmentType([])
|
|
61
|
-
expect(result).toStrictEqual({})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test("should handle undefined or null input gracefully", () => {
|
|
65
|
-
expect(augmentType(undefined)).toStrictEqual({})
|
|
66
|
-
expect(augmentType(null)).toStrictEqual({})
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("should augment a map of types correctly", () => {
|
|
70
|
-
const types = {
|
|
71
|
-
player: { onJump: () => "boing" },
|
|
72
|
-
enemy: [
|
|
73
|
-
{ onMove: () => "stomp" },
|
|
74
|
-
(type) => ({ ...type, onAttack: () => "bite" }),
|
|
75
|
-
],
|
|
76
|
-
item: [],
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result = augmentTypes(types)
|
|
80
|
-
|
|
81
|
-
expect(result.player).toStrictEqual({ onJump: expect.any(Function) })
|
|
82
|
-
expect(result.enemy).toStrictEqual({
|
|
83
|
-
onMove: expect.any(Function),
|
|
84
|
-
onAttack: expect.any(Function),
|
|
85
|
-
})
|
|
86
|
-
expect(result.item).toStrictEqual({})
|
|
87
|
-
})
|