@inglorious/store 3.0.0 → 4.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/README.md +9 -9
- package/package.json +9 -5
- package/src/middlewares.js +47 -0
- package/src/store.js +30 -20
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install @inglorious/store
|
|
|
21
21
|
|
|
22
22
|
The state management is built on a few simple principles:
|
|
23
23
|
|
|
24
|
-
1. **Entities and Properties**: The state is composed of **entities**, which are unique objects. Each entity has a **type** and a set of properties (e.g., `position:
|
|
24
|
+
1. **Entities and Properties**: The state is composed of **entities**, which are unique objects. Each entity has a **type** and a set of properties (e.g., `position: v(0, 0, 0)`, `health: 100`). Unlike a traditional ECS, properties are not grouped into explicit components.
|
|
25
25
|
|
|
26
26
|
2. **Types and Behaviors**: The logic for how entities and the overall state change is defined in **types** and **systems**.
|
|
27
27
|
- **Types** are arrays of **behaviors**. A behavior is an object that contains event handlers (e.g., `update(entity, dt) { ... }`). Behaviors are composable, allowing you to define a type by combining multiple sets of properties and event handlers.
|
|
@@ -31,7 +31,7 @@ The state management is built on a few simple principles:
|
|
|
31
31
|
- The store processes events by first applying behaviors defined on an entity's type, and then running the logic in the systems.
|
|
32
32
|
- An `update` event with a `dt` (delta time) payload is automatically dispatched on every `store.update()` call, making it suitable for a game loop.
|
|
33
33
|
|
|
34
|
-
4. **Immutability**: The state is immutable. Updates are handled internally by **[
|
|
34
|
+
4. **Immutability**: The state is immutable. Updates are handled internally by **[Mutative](https://mutative.js.org/)**, so you can "mutate" the state directly within a type's or system's behavior function, and a new, immutable state will be created.
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -101,7 +101,7 @@ Here is a simple example of a player entity that moves based on events.
|
|
|
101
101
|
|
|
102
102
|
```javascript
|
|
103
103
|
import { createStore, createApi } from "@inglorious/store"
|
|
104
|
-
import { add, scale } from "@inglorious/utils/math/
|
|
104
|
+
import { add, scale } from "@inglorious/utils/math/vectors.js"
|
|
105
105
|
|
|
106
106
|
// 1. Define the behaviors
|
|
107
107
|
const transform = {
|
|
@@ -133,8 +133,8 @@ const types = {
|
|
|
133
133
|
const entities = {
|
|
134
134
|
player1: {
|
|
135
135
|
type: "player",
|
|
136
|
-
position:
|
|
137
|
-
velocity:
|
|
136
|
+
position: v(0, 0, 0),
|
|
137
|
+
velocity: v(0.0625, 0, 0),
|
|
138
138
|
},
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -154,17 +154,17 @@ store.subscribe(() => {
|
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
// 7. Notify the store of an event
|
|
157
|
-
console.log("Initial player position:", selectPlayerPosition()) // =>
|
|
157
|
+
console.log("Initial player position:", selectPlayerPosition()) // => v(0, 0, 0)
|
|
158
158
|
|
|
159
159
|
// Dispatch a custom `move` event with a payload
|
|
160
160
|
api.notify("move", { id: "player1", dx: 5, dz: 5 })
|
|
161
161
|
|
|
162
162
|
// Events are queued but not yet processed
|
|
163
|
-
console.log("Position after notify:", selectPlayerPosition()) // =>
|
|
163
|
+
console.log("Position after notify:", selectPlayerPosition()) // => v(0, 0, 0)
|
|
164
164
|
|
|
165
165
|
// 8. Run the update loop to process the queue and trigger `update` behaviors
|
|
166
166
|
store.update(16) // Pass delta time
|
|
167
|
-
// Console output from subscriber: "State updated!
|
|
167
|
+
// Console output from subscriber: "State updated! v(6, 0, 5)"
|
|
168
168
|
|
|
169
|
-
console.log("Final position:", selectPlayerPosition()) // =>
|
|
169
|
+
console.log("Final position:", selectPlayerPosition()) // => v(6, 0, 5)
|
|
170
170
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "A state manager inspired by Redux, but tailored for the specific needs of game development.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,19 +27,23 @@
|
|
|
27
27
|
"./*": "./src/*"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"src
|
|
30
|
+
"src"
|
|
31
31
|
],
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
37
|
-
"@inglorious/utils": "
|
|
36
|
+
"mutative": "^1.3.0",
|
|
37
|
+
"@inglorious/utils": "3.5.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@inglorious/utils": "3.5.0"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"prettier": "^3.6.2",
|
|
41
44
|
"vite": "^7.1.3",
|
|
42
|
-
"vitest": "^1.6.1"
|
|
45
|
+
"vitest": "^1.6.1",
|
|
46
|
+
"@inglorious/eslint-config": "1.0.0"
|
|
43
47
|
},
|
|
44
48
|
"engines": {
|
|
45
49
|
"node": ">= 22"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { compose } from "@inglorious/utils/functions/functions.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Applies a list of middleware functions to a store's dispatch method.
|
|
5
|
+
* @param {...Function} middlewares The middleware functions to apply.
|
|
6
|
+
* @returns {Function} A store enhancer function.
|
|
7
|
+
*/
|
|
8
|
+
export function applyMiddlewares(...middlewares) {
|
|
9
|
+
return (store, baseApi) => {
|
|
10
|
+
let dispatch = () => {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Dispatching while constructing your middleware is not allowed.",
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Enhanced store interface that middlewares receive
|
|
17
|
+
const middlewareStore = {
|
|
18
|
+
getState: store.getState,
|
|
19
|
+
setState: store.setState,
|
|
20
|
+
dispatch: (...args) => dispatch(...args),
|
|
21
|
+
notify: (type, payload) => dispatch({ type, payload }),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Enhanced API interface that middlewares receive
|
|
25
|
+
const middlewareApi = {
|
|
26
|
+
...baseApi,
|
|
27
|
+
dispatch: (...args) => dispatch(...args),
|
|
28
|
+
notify: (type, payload) => dispatch({ type, payload }),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create middleware chain - each middleware gets the store and can access baseApi
|
|
32
|
+
const chain = middlewares.map((middleware) =>
|
|
33
|
+
middleware(middlewareStore, middlewareApi),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Compose the middleware chain to create enhanced dispatch
|
|
37
|
+
dispatch = compose(...chain)(store.dispatch)
|
|
38
|
+
|
|
39
|
+
// Return enhanced API that external code will use
|
|
40
|
+
return {
|
|
41
|
+
...middlewareApi,
|
|
42
|
+
// Override dispatch/notify with the middleware-enhanced versions
|
|
43
|
+
dispatch,
|
|
44
|
+
notify: (type, payload) => dispatch({ type, payload }),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/store.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { create } from "mutative"
|
|
2
2
|
|
|
3
|
+
import { createApi } from "./api.js"
|
|
3
4
|
import { augmentEntities, augmentEntity } from "./entities.js"
|
|
4
5
|
import { EventMap } from "./event-map.js"
|
|
6
|
+
import { applyMiddlewares } from "./middlewares.js"
|
|
5
7
|
import { augmentType, augmentTypes } from "./types.js"
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Creates a store to manage state and events.
|
|
9
|
-
*
|
|
10
11
|
* @param {Object} config - Configuration options for the store.
|
|
11
12
|
* @param {Object} [config.types] - The initial types configuration.
|
|
12
13
|
* @param {Object} [config.entities] - The initial entities configuration.
|
|
@@ -16,6 +17,7 @@ export function createStore({
|
|
|
16
17
|
types: originalTypes,
|
|
17
18
|
entities: originalEntities,
|
|
18
19
|
systems = [],
|
|
20
|
+
middlewares = [],
|
|
19
21
|
}) {
|
|
20
22
|
const listeners = new Set()
|
|
21
23
|
|
|
@@ -24,11 +26,12 @@ export function createStore({
|
|
|
24
26
|
let state, eventMap, incomingEvents
|
|
25
27
|
reset()
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
const baseStore = {
|
|
28
30
|
subscribe,
|
|
29
31
|
update,
|
|
30
32
|
notify,
|
|
31
33
|
dispatch, // needed for compatibility with Redux
|
|
34
|
+
getApi,
|
|
32
35
|
getTypes,
|
|
33
36
|
getOriginalTypes,
|
|
34
37
|
getState,
|
|
@@ -36,9 +39,16 @@ export function createStore({
|
|
|
36
39
|
reset,
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
const baseApi = createApi(baseStore)
|
|
43
|
+
|
|
44
|
+
const api = middlewares.length
|
|
45
|
+
? applyMiddlewares(...middlewares)(baseStore, baseApi)
|
|
46
|
+
: baseApi
|
|
47
|
+
|
|
48
|
+
return baseStore
|
|
49
|
+
|
|
39
50
|
/**
|
|
40
51
|
* Subscribes a listener to state updates.
|
|
41
|
-
*
|
|
42
52
|
* @param {Function} listener - The listener function to call on updates.
|
|
43
53
|
* @returns {Function} A function to unsubscribe the listener.
|
|
44
54
|
*/
|
|
@@ -52,14 +62,12 @@ export function createStore({
|
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* Updates the state based on elapsed time and processes events.
|
|
55
|
-
*
|
|
56
65
|
* @param {number} dt - The delta time since the last update in milliseconds.
|
|
57
|
-
* @param {Object} api - The engine's public API.
|
|
58
66
|
*/
|
|
59
|
-
function update(
|
|
67
|
+
function update() {
|
|
60
68
|
const processedEvents = []
|
|
61
69
|
|
|
62
|
-
state =
|
|
70
|
+
state = create(state, (draft) => {
|
|
63
71
|
while (incomingEvents.length) {
|
|
64
72
|
const event = incomingEvents.shift()
|
|
65
73
|
processedEvents.push(event)
|
|
@@ -67,7 +75,7 @@ export function createStore({
|
|
|
67
75
|
if (event.type === "morph") {
|
|
68
76
|
const { id, type } = event.payload
|
|
69
77
|
|
|
70
|
-
const entity =
|
|
78
|
+
const entity = draft.entities[id]
|
|
71
79
|
const oldType = types[entity.type]
|
|
72
80
|
|
|
73
81
|
originalTypes[id] = type
|
|
@@ -80,7 +88,7 @@ export function createStore({
|
|
|
80
88
|
|
|
81
89
|
if (event.type === "add") {
|
|
82
90
|
const { id, ...entity } = event.payload
|
|
83
|
-
|
|
91
|
+
draft.entities[id] = augmentEntity(id, entity)
|
|
84
92
|
const type = types[entity.type]
|
|
85
93
|
|
|
86
94
|
eventMap.addEntity(id, type)
|
|
@@ -89,9 +97,9 @@ export function createStore({
|
|
|
89
97
|
|
|
90
98
|
if (event.type === "remove") {
|
|
91
99
|
const id = event.payload
|
|
92
|
-
const entity =
|
|
100
|
+
const entity = draft.entities[id]
|
|
93
101
|
const type = types[entity.type]
|
|
94
|
-
delete
|
|
102
|
+
delete draft.entities[id]
|
|
95
103
|
|
|
96
104
|
eventMap.removeEntity(id, type)
|
|
97
105
|
incomingEvents.unshift({ type: "destroy", payload: id })
|
|
@@ -99,7 +107,7 @@ export function createStore({
|
|
|
99
107
|
|
|
100
108
|
const entityIds = eventMap.getEntitiesForEvent(event.type)
|
|
101
109
|
for (const id of entityIds) {
|
|
102
|
-
const entity =
|
|
110
|
+
const entity = draft.entities[id]
|
|
103
111
|
const type = types[entity.type]
|
|
104
112
|
const handle = type[event.type]
|
|
105
113
|
handle(entity, event.payload, api)
|
|
@@ -107,7 +115,7 @@ export function createStore({
|
|
|
107
115
|
|
|
108
116
|
systems.forEach((system) => {
|
|
109
117
|
const handle = system[event.type]
|
|
110
|
-
handle?.(
|
|
118
|
+
handle?.(draft, event.payload, api)
|
|
111
119
|
})
|
|
112
120
|
}
|
|
113
121
|
})
|
|
@@ -119,7 +127,6 @@ export function createStore({
|
|
|
119
127
|
|
|
120
128
|
/**
|
|
121
129
|
* Notifies the store of a new event.
|
|
122
|
-
*
|
|
123
130
|
* @param {string} type - The event object type to notify.
|
|
124
131
|
* @param {any} payload - The event object payload to notify.
|
|
125
132
|
*/
|
|
@@ -129,7 +136,6 @@ export function createStore({
|
|
|
129
136
|
|
|
130
137
|
/**
|
|
131
138
|
* Dispatches an event to be processed in the next update cycle.
|
|
132
|
-
*
|
|
133
139
|
* @param {Object} event - The event object.
|
|
134
140
|
* @param {string} event.type - The type of the event.
|
|
135
141
|
* @param {any} [event.payload] - The payload of the event.
|
|
@@ -138,10 +144,17 @@ export function createStore({
|
|
|
138
144
|
incomingEvents.push(event)
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Retrieves the store's API, which includes methods for interacting with the store.
|
|
149
|
+
* @returns {Object} The API object for the store.
|
|
150
|
+
*/
|
|
151
|
+
function getApi() {
|
|
152
|
+
return api
|
|
153
|
+
}
|
|
154
|
+
|
|
141
155
|
/**
|
|
142
156
|
* Retrieves the augmented types configuration.
|
|
143
157
|
* This includes composed behaviors and event handlers wrapped for immutability.
|
|
144
|
-
*
|
|
145
158
|
* @returns {Object} The augmented types configuration.
|
|
146
159
|
*/
|
|
147
160
|
function getTypes() {
|
|
@@ -150,7 +163,6 @@ export function createStore({
|
|
|
150
163
|
|
|
151
164
|
/**
|
|
152
165
|
* Retrieves the original, un-augmented types configuration.
|
|
153
|
-
*
|
|
154
166
|
* @returns {Object} The original types configuration.
|
|
155
167
|
*/
|
|
156
168
|
function getOriginalTypes() {
|
|
@@ -159,7 +171,6 @@ export function createStore({
|
|
|
159
171
|
|
|
160
172
|
/**
|
|
161
173
|
* Retrieves the current state.
|
|
162
|
-
*
|
|
163
174
|
* @returns {Object} The current state.
|
|
164
175
|
*/
|
|
165
176
|
function getState() {
|
|
@@ -169,7 +180,6 @@ export function createStore({
|
|
|
169
180
|
/**
|
|
170
181
|
* Sets the entire state of the store.
|
|
171
182
|
* This is useful for importing state or setting initial state from a server.
|
|
172
|
-
*
|
|
173
183
|
* @param {Object} nextState - The new state to set.
|
|
174
184
|
*/
|
|
175
185
|
function setState(nextState) {
|