@inglorious/store 7.0.0 → 7.1.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/README.md +5 -3
- package/package.json +2 -2
- package/src/api.js +1 -0
- package/src/client/devtools.js +17 -11
- package/src/event-map.js +96 -20
- package/src/event-map.test.js +79 -33
- package/src/store.js +56 -19
- package/src/store.test.js +1 -1
package/README.md
CHANGED
|
@@ -477,6 +477,7 @@ Notice: you don't need pending/fulfilled/rejected actions. You stay in control o
|
|
|
477
477
|
- **`api.notify(type, payload)`** - trigger other events (queued, not immediate)
|
|
478
478
|
- **`api.dispatch(action)`** - optional, if you prefer Redux-style dispatching
|
|
479
479
|
- **`api.getTypes()`** - access type definitions (mainly for middleware/plugins)
|
|
480
|
+
- **`api.getType(typeName)`** - access type definition (mainly for overrides)
|
|
480
481
|
|
|
481
482
|
All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
|
|
482
483
|
|
|
@@ -635,10 +636,10 @@ When multiple behaviors define the same event, they all run in order. This allow
|
|
|
635
636
|
|
|
636
637
|
### ⏱️ Batched Mode
|
|
637
638
|
|
|
638
|
-
The Inglorious Store features an **event queue**. In the default `
|
|
639
|
+
The Inglorious Store features an **event queue**. In the default `auto` update mode, each notified event will trigger and update of the state (same as Redux). But in `manual` update mode, you can process multiple events together before re-rendering:
|
|
639
640
|
|
|
640
641
|
```javascript
|
|
641
|
-
const store = createStore({ types, entities,
|
|
642
|
+
const store = createStore({ types, entities, updateMode: "manual" })
|
|
642
643
|
|
|
643
644
|
// add events to the event queue
|
|
644
645
|
store.notify("playerMoved", { x: 100, y: 50 })
|
|
@@ -677,7 +678,7 @@ const store = createStore({
|
|
|
677
678
|
types, // Object: entity type definitions
|
|
678
679
|
entities, // Object: initial entities
|
|
679
680
|
systems, // Array (optional): global state handlers
|
|
680
|
-
|
|
681
|
+
updateMode, // String (optional): 'auto' (default) or 'manual'
|
|
681
682
|
})
|
|
682
683
|
```
|
|
683
684
|
|
|
@@ -718,6 +719,7 @@ Each handler receives three arguments:
|
|
|
718
719
|
- `notify(type, payload)` - trigger other events
|
|
719
720
|
- `dispatch(action)` - optional, if you prefer Redux-style dispatching
|
|
720
721
|
- `getTypes()` - type definitions (for middleware)
|
|
722
|
+
- `getType(typeName)` - type definition (for overriding)
|
|
721
723
|
|
|
722
724
|
### Built-in Lifecycle Events
|
|
723
725
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"prettier": "^3.6.2",
|
|
59
59
|
"vite": "^7.1.3",
|
|
60
60
|
"vitest": "^1.6.1",
|
|
61
|
-
"@inglorious/eslint-config": "1.0
|
|
61
|
+
"@inglorious/eslint-config": "1.1.0"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">= 22"
|
package/src/api.js
CHANGED
package/src/client/devtools.js
CHANGED
|
@@ -12,7 +12,7 @@ const globalContainer = import.meta.hot
|
|
|
12
12
|
// devToolsInstance,
|
|
13
13
|
// unsubscribe,
|
|
14
14
|
// store,
|
|
15
|
-
//
|
|
15
|
+
// updateMode,
|
|
16
16
|
// restoreSetState: () => void,
|
|
17
17
|
// }
|
|
18
18
|
|
|
@@ -45,18 +45,18 @@ export function createDevtools(config = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function connectDevTools(store, config = {}) {
|
|
48
|
-
const
|
|
48
|
+
const updateMode = config.updateMode ?? "auto"
|
|
49
49
|
|
|
50
|
-
// Prevent duplicate connections; rewire if store or mode changed
|
|
50
|
+
// Prevent duplicate connections; rewire if store or update mode changed
|
|
51
51
|
const existing = getConnection()
|
|
52
52
|
if (existing) {
|
|
53
|
-
const sameMode = existing.
|
|
53
|
+
const sameMode = existing.updateMode === updateMode
|
|
54
54
|
if (sameMode) {
|
|
55
55
|
// Hot-swap store without resetting DevTools history
|
|
56
56
|
existing.restoreSetState?.()
|
|
57
57
|
const baseSetState = store.setState
|
|
58
58
|
let restoreSetState = null
|
|
59
|
-
if (
|
|
59
|
+
if (updateMode === "auto") {
|
|
60
60
|
restoreSetState = () => {
|
|
61
61
|
if (store.setState !== baseSetState) {
|
|
62
62
|
store.setState = baseSetState
|
|
@@ -75,7 +75,7 @@ function connectDevTools(store, config = {}) {
|
|
|
75
75
|
existing.restoreSetState = restoreSetState
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Different mode: fully disconnect previous and proceed
|
|
78
|
+
// Different update mode: fully disconnect previous and proceed
|
|
79
79
|
try {
|
|
80
80
|
existing.unsubscribe?.()
|
|
81
81
|
} finally {
|
|
@@ -91,8 +91,8 @@ function connectDevTools(store, config = {}) {
|
|
|
91
91
|
const name = config.name ?? document.title
|
|
92
92
|
const baseSetState = store.setState
|
|
93
93
|
let restoreSetState = null
|
|
94
|
-
// Only add setState side-effects in
|
|
95
|
-
if (
|
|
94
|
+
// Only add setState side-effects in auto update mode; manual update mode logs explicitly from engine.
|
|
95
|
+
if (updateMode === "auto") {
|
|
96
96
|
restoreSetState = () => {
|
|
97
97
|
if (store.setState !== baseSetState) {
|
|
98
98
|
store.setState = baseSetState
|
|
@@ -140,7 +140,13 @@ function connectDevTools(store, config = {}) {
|
|
|
140
140
|
|
|
141
141
|
devToolsInstance.init(store.getState())
|
|
142
142
|
|
|
143
|
-
setConnection({
|
|
143
|
+
setConnection({
|
|
144
|
+
devToolsInstance,
|
|
145
|
+
unsubscribe,
|
|
146
|
+
store,
|
|
147
|
+
updateMode,
|
|
148
|
+
restoreSetState,
|
|
149
|
+
})
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
function disconnectDevTools() {
|
|
@@ -234,12 +240,12 @@ function handleAction(message) {
|
|
|
234
240
|
|
|
235
241
|
function shouldLogEvent(event, config) {
|
|
236
242
|
const {
|
|
237
|
-
|
|
243
|
+
updateMode = "auto",
|
|
238
244
|
blacklist = [],
|
|
239
245
|
whitelist = [],
|
|
240
246
|
filter = null,
|
|
241
247
|
} = config
|
|
242
|
-
if (
|
|
248
|
+
if (updateMode !== "auto") return false
|
|
243
249
|
const passesBlacklist = !blacklist.length || !blacklist.includes(event.type)
|
|
244
250
|
const passesWhitelist = !whitelist.length || whitelist.includes(event.type)
|
|
245
251
|
const passesFilter = !filter || filter(event)
|
package/src/event-map.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* @typedef {Object.<string, any>} Entity - An object representing a entity.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const SPLIT_LIMIT = 2
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* A class to manage the mapping of event names to the entity IDs that handle them.
|
|
8
|
-
* This is used for optimized event handling.
|
|
10
|
+
* This is used for optimized event handling with support for scoped events.
|
|
9
11
|
*/
|
|
10
12
|
export class EventMap {
|
|
11
13
|
/**
|
|
@@ -16,17 +18,26 @@ export class EventMap {
|
|
|
16
18
|
*/
|
|
17
19
|
constructor(types, entities) {
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
21
|
+
* Maps handler names to type names to Sets of entity IDs.
|
|
22
|
+
* Structure: handlerName -> typeName -> Set<entityId>
|
|
23
|
+
* Example: 'submit' -> 'form' -> Set(['loginForm', 'signupForm'])
|
|
24
|
+
*
|
|
25
|
+
* @type {Map<string, Map<string, Set<string>>>}
|
|
26
|
+
*/
|
|
27
|
+
this.handlerToTypeToEntities = new Map()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maps entity IDs to their type names for quick lookup.
|
|
20
31
|
*
|
|
21
|
-
* @type {
|
|
32
|
+
* @type {Map<string, string>}
|
|
22
33
|
*/
|
|
23
|
-
this.
|
|
34
|
+
this.entityTypes = new Map()
|
|
24
35
|
|
|
25
36
|
for (const entityId in entities) {
|
|
26
37
|
const entity = entities[entityId]
|
|
27
38
|
const type = types[entity.type]
|
|
28
39
|
if (type) {
|
|
29
|
-
this.addEntity(entityId, type)
|
|
40
|
+
this.addEntity(entityId, type, entity.type)
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
}
|
|
@@ -37,13 +48,24 @@ export class EventMap {
|
|
|
37
48
|
*
|
|
38
49
|
* @param {string} entityId - The ID of the entity.
|
|
39
50
|
* @param {Type} type - The augmented type object of the entity.
|
|
51
|
+
* @param {string} typeName - The name of the entity's type.
|
|
40
52
|
*/
|
|
41
|
-
addEntity(entityId, type) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
addEntity(entityId, type, typeName) {
|
|
54
|
+
this.entityTypes.set(entityId, typeName)
|
|
55
|
+
|
|
56
|
+
for (const handlerName in type) {
|
|
57
|
+
if (typeof type[handlerName] !== "function") continue
|
|
58
|
+
|
|
59
|
+
if (!this.handlerToTypeToEntities.has(handlerName)) {
|
|
60
|
+
this.handlerToTypeToEntities.set(handlerName, new Map())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const typeMap = this.handlerToTypeToEntities.get(handlerName)
|
|
64
|
+
if (!typeMap.has(typeName)) {
|
|
65
|
+
typeMap.set(typeName, new Set())
|
|
45
66
|
}
|
|
46
|
-
|
|
67
|
+
|
|
68
|
+
typeMap.get(typeName).add(entityId)
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
71
|
|
|
@@ -53,23 +75,77 @@ export class EventMap {
|
|
|
53
75
|
*
|
|
54
76
|
* @param {string} entityId - The ID of the entity.
|
|
55
77
|
* @param {Type} type - The augmented type object of the entity.
|
|
78
|
+
* @param {string} typeName - The name of the entity's type.
|
|
56
79
|
*/
|
|
57
|
-
removeEntity(entityId, type) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
removeEntity(entityId, type, typeName) {
|
|
81
|
+
this.entityTypes.delete(entityId)
|
|
82
|
+
|
|
83
|
+
for (const handlerName in type) {
|
|
84
|
+
const typeMap = this.handlerToTypeToEntities.get(handlerName)
|
|
85
|
+
if (typeMap) {
|
|
86
|
+
const entitySet = typeMap.get(typeName)
|
|
87
|
+
if (entitySet) {
|
|
88
|
+
entitySet.delete(entityId)
|
|
89
|
+
}
|
|
62
90
|
}
|
|
63
91
|
}
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
/**
|
|
67
|
-
* Retrieves the
|
|
95
|
+
* Retrieves the array of entity IDs that should handle a given event.
|
|
96
|
+
* Supports scoped events:
|
|
97
|
+
* - 'submit' -> all entities with 'submit' handler
|
|
98
|
+
* - 'form:submit' -> all form entities with 'submit' handler
|
|
99
|
+
* - 'form#loginForm:submit' -> only loginForm entity (of type form)
|
|
100
|
+
* - '#loginForm:submit' -> only loginForm entity
|
|
68
101
|
*
|
|
69
|
-
* @param {string}
|
|
70
|
-
* @returns {
|
|
102
|
+
* @param {string} eventString - The event string (e.g., 'submit', 'form:submit', 'form[id]:submit')
|
|
103
|
+
* @returns {string[]} An array of entity IDs that should handle this event.
|
|
71
104
|
*/
|
|
72
|
-
getEntitiesForEvent(
|
|
73
|
-
|
|
105
|
+
getEntitiesForEvent(eventString) {
|
|
106
|
+
const {
|
|
107
|
+
type: targetType,
|
|
108
|
+
entityId: targetEntityId,
|
|
109
|
+
event: handlerName,
|
|
110
|
+
} = parseEvent(eventString)
|
|
111
|
+
|
|
112
|
+
const typeMap = this.handlerToTypeToEntities.get(handlerName)
|
|
113
|
+
if (!typeMap) return []
|
|
114
|
+
|
|
115
|
+
if (targetEntityId) {
|
|
116
|
+
const type = targetType ?? this.entityTypes.get(targetEntityId)
|
|
117
|
+
const entitySet = typeMap.get(type)
|
|
118
|
+
return entitySet && entitySet.has(targetEntityId) ? [targetEntityId] : []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// form:submit - all entities of this type
|
|
122
|
+
if (targetType) {
|
|
123
|
+
const entitySet = typeMap.get(targetType)
|
|
124
|
+
return entitySet ? Array.from(entitySet) : []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// submit - all entities with this handler (broadcast)
|
|
128
|
+
const allEntities = []
|
|
129
|
+
for (const entitySet of typeMap.values()) {
|
|
130
|
+
allEntities.push(...entitySet)
|
|
131
|
+
}
|
|
132
|
+
return allEntities
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parses an event string into its components.
|
|
138
|
+
* @param {string} eventString - The event string (e.g., 'submit', 'form:submit', 'form#loginForm:submit')
|
|
139
|
+
* @returns {{ type: string|null, entityId: string|null, event: string }}
|
|
140
|
+
*/
|
|
141
|
+
export function parseEvent(eventString) {
|
|
142
|
+
const [left, event] = eventString.split(":", SPLIT_LIMIT)
|
|
143
|
+
if (!event) return { type: null, entityId: null, event: left }
|
|
144
|
+
|
|
145
|
+
const [type, entityId] = left.split("#", SPLIT_LIMIT)
|
|
146
|
+
return {
|
|
147
|
+
type: type || null,
|
|
148
|
+
entityId: entityId || null,
|
|
149
|
+
event,
|
|
74
150
|
}
|
|
75
151
|
}
|
package/src/event-map.test.js
CHANGED
|
@@ -25,17 +25,20 @@ test("constructor should initialize the event map from types and entities", () =
|
|
|
25
25
|
|
|
26
26
|
const eventMap = new EventMap(types, entities)
|
|
27
27
|
|
|
28
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
28
|
+
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual([
|
|
29
|
+
"player1",
|
|
30
|
+
"player2",
|
|
31
|
+
"enemy1",
|
|
32
|
+
])
|
|
33
|
+
expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual([
|
|
34
|
+
"player1",
|
|
35
|
+
"player2",
|
|
36
|
+
])
|
|
34
37
|
|
|
35
38
|
// 'item' type has no events, so it shouldn't be in the map
|
|
36
|
-
expect(eventMap.getEntitiesForEvent("item")).toStrictEqual(
|
|
39
|
+
expect(eventMap.getEntitiesForEvent("item")).toStrictEqual([])
|
|
37
40
|
// 'ghost' type doesn't exist, so it should be ignored
|
|
38
|
-
expect(eventMap.getEntitiesForEvent("ghost")).toStrictEqual(
|
|
41
|
+
expect(eventMap.getEntitiesForEvent("ghost")).toStrictEqual([])
|
|
39
42
|
})
|
|
40
43
|
|
|
41
44
|
test("addEntity should add an entity to the correct event sets", () => {
|
|
@@ -47,23 +50,21 @@ test("addEntity should add an entity to the correct event sets", () => {
|
|
|
47
50
|
}
|
|
48
51
|
const eventMap = new EventMap(types, {})
|
|
49
52
|
|
|
50
|
-
eventMap.addEntity("player1", types.player)
|
|
53
|
+
eventMap.addEntity("player1", types.player, "player")
|
|
51
54
|
|
|
52
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(
|
|
56
|
-
new Set(["player1"]),
|
|
57
|
-
)
|
|
55
|
+
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player1"])
|
|
56
|
+
expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(["player1"])
|
|
58
57
|
|
|
59
58
|
// Add another entity of the same type
|
|
60
|
-
eventMap.addEntity("player2", types.player)
|
|
61
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
])
|
|
67
68
|
})
|
|
68
69
|
|
|
69
70
|
test("removeEntity should remove an entity from its event sets", () => {
|
|
@@ -79,17 +80,15 @@ test("removeEntity should remove an entity from its event sets", () => {
|
|
|
79
80
|
}
|
|
80
81
|
const eventMap = new EventMap(types, entities)
|
|
81
82
|
|
|
82
|
-
eventMap.removeEntity("player1", types.player)
|
|
83
|
+
eventMap.removeEntity("player1", types.player, "player")
|
|
83
84
|
|
|
84
|
-
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(
|
|
88
|
-
new Set(["player2"]),
|
|
89
|
-
)
|
|
85
|
+
expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player2"])
|
|
86
|
+
expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(["player2"])
|
|
90
87
|
|
|
91
88
|
// Removing a non-existent entity should not throw an error
|
|
92
|
-
expect(() =>
|
|
89
|
+
expect(() =>
|
|
90
|
+
eventMap.removeEntity("player3", types.player, "player"),
|
|
91
|
+
).not.toThrow()
|
|
93
92
|
})
|
|
94
93
|
|
|
95
94
|
test("getEntitiesForEvent should return the correct set of entities for an event", () => {
|
|
@@ -104,10 +103,57 @@ test("getEntitiesForEvent should return the correct set of entities for an event
|
|
|
104
103
|
const eventMap = new EventMap(types, entities)
|
|
105
104
|
|
|
106
105
|
const updateEntities = eventMap.getEntitiesForEvent("update")
|
|
107
|
-
expect(updateEntities).toStrictEqual(
|
|
106
|
+
expect(updateEntities).toStrictEqual(["player1", "enemy1"])
|
|
108
107
|
|
|
109
108
|
const fireEntities = eventMap.getEntitiesForEvent("fire")
|
|
110
|
-
expect(fireEntities).toStrictEqual(
|
|
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
|
+
])
|
|
111
157
|
})
|
|
112
158
|
|
|
113
159
|
test("EventMap provides a significant performance benefit for event handling", async () => {
|
|
@@ -145,7 +191,7 @@ test("EventMap provides a significant performance benefit for event handling", a
|
|
|
145
191
|
|
|
146
192
|
// Assertions to verify correctness
|
|
147
193
|
expect(oldWayTime).toBeGreaterThan(newWayTime)
|
|
148
|
-
expect(updateHandler).toHaveBeenCalledTimes(updaterIds.
|
|
194
|
+
expect(updateHandler).toHaveBeenCalledTimes(updaterIds.length)
|
|
149
195
|
})
|
|
150
196
|
|
|
151
197
|
// Helper function to create a large set of test entities
|
package/src/store.js
CHANGED
|
@@ -2,7 +2,7 @@ import { create } from "mutative"
|
|
|
2
2
|
|
|
3
3
|
import { createApi } from "./api.js"
|
|
4
4
|
import { augmentEntities, augmentEntity } from "./entities.js"
|
|
5
|
-
import { EventMap } from "./event-map.js"
|
|
5
|
+
import { EventMap, parseEvent } from "./event-map.js"
|
|
6
6
|
import { applyMiddlewares } from "./middlewares.js"
|
|
7
7
|
import { augmentType, augmentTypes } from "./types.js"
|
|
8
8
|
|
|
@@ -11,9 +11,9 @@ import { augmentType, augmentTypes } from "./types.js"
|
|
|
11
11
|
* @param {Object} config - Configuration options for the store.
|
|
12
12
|
* @param {Object} [config.types] - The initial types configuration.
|
|
13
13
|
* @param {Object} [config.entities] - The initial entities configuration.
|
|
14
|
-
* @param {Array} [config.
|
|
14
|
+
* @param {Array} [config.systems] - The initial systems configuration.
|
|
15
15
|
* @param {Array} [config.middlewares] - The initial middlewares configuration.
|
|
16
|
-
* @param {"
|
|
16
|
+
* @param {"auto" | "manual"} [config.updateMode] - The update mode (defaults to "auto").
|
|
17
17
|
* @returns {Object} The store with methods to interact with state and events.
|
|
18
18
|
*/
|
|
19
19
|
export function createStore({
|
|
@@ -21,13 +21,13 @@ export function createStore({
|
|
|
21
21
|
entities: originalEntities,
|
|
22
22
|
systems = [],
|
|
23
23
|
middlewares = [],
|
|
24
|
-
|
|
24
|
+
updateMode = "auto",
|
|
25
25
|
}) {
|
|
26
26
|
const listeners = new Set()
|
|
27
27
|
|
|
28
28
|
const types = augmentTypes(originalTypes)
|
|
29
29
|
|
|
30
|
-
let state, eventMap, incomingEvents
|
|
30
|
+
let state, eventMap, incomingEvents, isProcessing
|
|
31
31
|
reset()
|
|
32
32
|
|
|
33
33
|
const baseStore = {
|
|
@@ -36,6 +36,7 @@ export function createStore({
|
|
|
36
36
|
notify,
|
|
37
37
|
dispatch, // needed for compatibility with Redux
|
|
38
38
|
getTypes,
|
|
39
|
+
getType,
|
|
39
40
|
getState,
|
|
40
41
|
setState,
|
|
41
42
|
reset,
|
|
@@ -63,15 +64,21 @@ export function createStore({
|
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Updates the state based on elapsed time and processes events.
|
|
66
|
-
* @param {number} dt - The delta time since the last update in milliseconds.
|
|
67
67
|
*/
|
|
68
68
|
function update() {
|
|
69
|
+
if (isProcessing) {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isProcessing = true
|
|
69
74
|
const processedEvents = []
|
|
70
75
|
|
|
71
76
|
state = create(state, patcher, {
|
|
72
77
|
enableAutoFreeze: state.game?.devMode,
|
|
73
78
|
})
|
|
74
79
|
|
|
80
|
+
isProcessing = false
|
|
81
|
+
|
|
75
82
|
listeners.forEach((onUpdate) => onUpdate())
|
|
76
83
|
|
|
77
84
|
return processedEvents
|
|
@@ -81,18 +88,21 @@ export function createStore({
|
|
|
81
88
|
const event = incomingEvents.shift()
|
|
82
89
|
processedEvents.push(event)
|
|
83
90
|
|
|
91
|
+
// Handle special system events
|
|
84
92
|
if (event.type === "morph") {
|
|
85
93
|
const { id, type } = event.payload
|
|
86
94
|
|
|
87
95
|
const entity = draft[id]
|
|
88
96
|
const oldType = types[entity.type]
|
|
97
|
+
const oldTypeName = entity.type
|
|
89
98
|
|
|
90
|
-
originalTypes[
|
|
91
|
-
types[
|
|
92
|
-
const newType = types[
|
|
99
|
+
originalTypes[entity.type] = type
|
|
100
|
+
types[entity.type] = augmentType(originalTypes[entity.type])
|
|
101
|
+
const newType = types[entity.type]
|
|
93
102
|
|
|
94
|
-
eventMap.removeEntity(id, oldType)
|
|
95
|
-
eventMap.addEntity(id, newType)
|
|
103
|
+
eventMap.removeEntity(id, oldType, oldTypeName)
|
|
104
|
+
eventMap.addEntity(id, newType, oldTypeName) // Use the original type name
|
|
105
|
+
continue
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
if (event.type === "add") {
|
|
@@ -100,30 +110,42 @@ export function createStore({
|
|
|
100
110
|
draft[id] = augmentEntity(id, entity)
|
|
101
111
|
const type = types[entity.type]
|
|
102
112
|
|
|
103
|
-
eventMap.addEntity(id, type)
|
|
113
|
+
eventMap.addEntity(id, type, entity.type)
|
|
104
114
|
incomingEvents.unshift({ type: "create", payload: id })
|
|
115
|
+
continue
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
if (event.type === "remove") {
|
|
108
119
|
const id = event.payload
|
|
109
120
|
const entity = draft[id]
|
|
110
121
|
const type = types[entity.type]
|
|
122
|
+
const typeName = entity.type
|
|
111
123
|
delete draft[id]
|
|
112
124
|
|
|
113
|
-
eventMap.removeEntity(id, type)
|
|
125
|
+
eventMap.removeEntity(id, type, typeName)
|
|
114
126
|
incomingEvents.unshift({ type: "destroy", payload: id })
|
|
127
|
+
continue
|
|
115
128
|
}
|
|
116
129
|
|
|
130
|
+
// Parse the event to get handler name
|
|
131
|
+
const { event: handlerName } = parseEvent(event.type)
|
|
132
|
+
|
|
133
|
+
// Get entities that should handle this event (filtered by EventMap)
|
|
117
134
|
const entityIds = eventMap.getEntitiesForEvent(event.type)
|
|
135
|
+
|
|
118
136
|
for (const id of entityIds) {
|
|
119
137
|
const entity = draft[id]
|
|
120
138
|
const type = types[entity.type]
|
|
121
|
-
const handle = type[
|
|
122
|
-
|
|
139
|
+
const handle = type[handlerName]
|
|
140
|
+
|
|
141
|
+
if (handle) {
|
|
142
|
+
handle(entity, event.payload, api)
|
|
143
|
+
}
|
|
123
144
|
}
|
|
124
145
|
|
|
146
|
+
// Systems process events by handler name (not scoped)
|
|
125
147
|
systems.forEach((system) => {
|
|
126
|
-
const handle = system[
|
|
148
|
+
const handle = system[handlerName]
|
|
127
149
|
handle?.(draft, event.payload, api)
|
|
128
150
|
})
|
|
129
151
|
}
|
|
@@ -132,8 +154,13 @@ export function createStore({
|
|
|
132
154
|
|
|
133
155
|
/**
|
|
134
156
|
* Notifies the store of a new event.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
157
|
+
* Supports scoped events:
|
|
158
|
+
* - 'submit' - broadcast to all entities with submit handler
|
|
159
|
+
* - 'form:submit' - only form entities
|
|
160
|
+
* - 'form[loginForm]:submit' - only loginForm entity
|
|
161
|
+
*
|
|
162
|
+
* @param {string} type - The event type to notify.
|
|
163
|
+
* @param {any} payload - The event payload.
|
|
137
164
|
*/
|
|
138
165
|
function notify(type, payload) {
|
|
139
166
|
// NOTE: it's important to invoke store.dispatch instead of dispatch, otherwise we cannot override it
|
|
@@ -148,7 +175,7 @@ export function createStore({
|
|
|
148
175
|
*/
|
|
149
176
|
function dispatch(event) {
|
|
150
177
|
incomingEvents.push(event)
|
|
151
|
-
if (
|
|
178
|
+
if (updateMode === "auto") {
|
|
152
179
|
update()
|
|
153
180
|
}
|
|
154
181
|
}
|
|
@@ -162,6 +189,15 @@ export function createStore({
|
|
|
162
189
|
return types
|
|
163
190
|
}
|
|
164
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Retrieves an augmented type configuration given its name.
|
|
194
|
+
* @param {string} typeName - The type of the entity.
|
|
195
|
+
* @returns {Object} The augmented type configuration.
|
|
196
|
+
*/
|
|
197
|
+
function getType(typeName) {
|
|
198
|
+
return types[typeName]
|
|
199
|
+
}
|
|
200
|
+
|
|
165
201
|
/**
|
|
166
202
|
* Retrieves the current state.
|
|
167
203
|
* @returns {Object} The current state.
|
|
@@ -182,6 +218,7 @@ export function createStore({
|
|
|
182
218
|
state = newEntities
|
|
183
219
|
eventMap = new EventMap(types, nextState)
|
|
184
220
|
incomingEvents = []
|
|
221
|
+
isProcessing = false
|
|
185
222
|
|
|
186
223
|
const oldEntityIds = new Set(Object.keys(oldEntities))
|
|
187
224
|
const newEntityIds = new Set(Object.keys(newEntities))
|
package/src/store.test.js
CHANGED