@inglorious/store 5.5.0 → 6.0.0
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 +381 -649
- package/package.json +1 -1
- package/src/api.js +3 -9
- package/src/client/dev-tools.js +10 -0
- package/src/store.js +3 -1
package/README.md
CHANGED
|
@@ -3,203 +3,180 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@inglorious/store)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
A Redux-compatible state management library inspired by game development architecture.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Why settle for state management that wasn't designed for real-time sync? Games solved distributed state synchronization decades ago. Now you can use the same proven patterns for your apps.
|
|
8
|
+
**Drop-in replacement for Redux.** Works with `react-redux` and Redux DevTools. Adds entity-based state management (ECS) for simpler, more predictable code.
|
|
11
9
|
|
|
12
10
|
---
|
|
13
11
|
|
|
14
|
-
## Why
|
|
15
|
-
|
|
16
|
-
Games solved the hardest real-time problems: syncing state across laggy networks with hundreds of players at 60fps. They use:
|
|
12
|
+
## Why Inglorious Store?
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
- **Event queues** - natural ordering and conflict resolution
|
|
20
|
-
- **Serializable state** - trivial to send over the network
|
|
21
|
-
- **Client-side prediction** - responsive UIs that stay in sync
|
|
14
|
+
Redux is powerful but verbose. You need action creators, reducers, middleware for async operations, and a bunch of decisions about where logic should live. Redux Toolkit cuts the boilerplate, but you're still writing a lot of ceremony.
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
Inglorious Store ditches the ceremony entirely with **entity-based architecture** inspired by game engines. The same ECS patterns that power AAA games power your state management.
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
- Live updates (dashboards, chat)
|
|
27
|
-
- Undo/redo and time-travel debugging
|
|
28
|
-
- Multiplayer features
|
|
18
|
+
**Key benefits:**
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
- ✅ Drop-in Redux replacement (same API with `react-redux`)
|
|
21
|
+
- ✅ Entity-based state (manage multiple instances effortlessly)
|
|
22
|
+
- ✅ No action creators, thunks, or slices
|
|
23
|
+
- ✅ Predictable, testable, purely functional code
|
|
24
|
+
- ✅ Built-in lifecycle events (`add`, `remove`, `morph`)
|
|
25
|
+
- ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
|
|
31
26
|
|
|
32
27
|
---
|
|
33
28
|
|
|
34
29
|
## Installation
|
|
35
30
|
|
|
36
31
|
```bash
|
|
37
|
-
npm install @inglorious/store
|
|
32
|
+
npm install @inglorious/store react-redux
|
|
38
33
|
```
|
|
39
34
|
|
|
40
|
-
**For React
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm install @inglorious/react-store
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
See [@inglorious/react-store](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/react-store) for React-specific documentation.
|
|
35
|
+
**For React:** Works with standard `react-redux` without any extra packages.
|
|
47
36
|
|
|
48
37
|
---
|
|
49
38
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
Inglorious Store supports two update modes:
|
|
39
|
+
## Quick Comparison: Redux vs RTK vs Inglorious Store
|
|
53
40
|
|
|
54
|
-
###
|
|
41
|
+
### Redux
|
|
55
42
|
|
|
56
43
|
```javascript
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
44
|
+
// Action creators
|
|
45
|
+
const addTodo = (text) => ({ type: "ADD_TODO", payload: text })
|
|
46
|
+
|
|
47
|
+
// Reducer
|
|
48
|
+
const todosReducer = (state = [], action) => {
|
|
49
|
+
switch (action.type) {
|
|
50
|
+
case "ADD_TODO":
|
|
51
|
+
return [...state, { id: Date.now(), text: action.payload }]
|
|
52
|
+
default:
|
|
53
|
+
return state
|
|
54
|
+
}
|
|
55
|
+
}
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
const store =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
store.update() // Process all queued events at once
|
|
57
|
+
// Store setup
|
|
58
|
+
const store = configureStore({
|
|
59
|
+
reducer: { todos: todosReducer },
|
|
60
|
+
})
|
|
73
61
|
```
|
|
74
62
|
|
|
75
|
-
|
|
63
|
+
### Redux Toolkit
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
```javascript
|
|
66
|
+
const todosSlice = createSlice({
|
|
67
|
+
name: "todos",
|
|
68
|
+
initialState: [],
|
|
69
|
+
reducers: {
|
|
70
|
+
addTodo: (state, action) => {
|
|
71
|
+
state.push({ id: Date.now(), text: action.payload })
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
extraReducers: (builder) => {
|
|
75
|
+
builder.addCase(otherAction, (state, action) => {
|
|
76
|
+
// Handle action from other slice
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
})
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
const store = configureStore({
|
|
82
|
+
reducer: { todos: todosSlice.reducer },
|
|
83
|
+
})
|
|
84
|
+
```
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
### Inglorious Store
|
|
85
87
|
|
|
86
88
|
```javascript
|
|
89
|
+
// Define entity types and their behavior
|
|
87
90
|
const types = {
|
|
88
91
|
todoList: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const todos = await response.json()
|
|
92
|
-
|
|
93
|
-
// This event will be processed in the same update cycle
|
|
94
|
-
api.notify("todosReceived", todos)
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
todosReceived(entity, todos) {
|
|
98
|
-
entity.todos = todos
|
|
99
|
-
entity.loading = false
|
|
92
|
+
addTodo(entity, text) {
|
|
93
|
+
entity.todos.push({ id: Date.now(), text })
|
|
100
94
|
},
|
|
101
95
|
},
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
In eager mode, only `fetchTodos` would process, and `todosReceived` would be ignored.
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## Key Features
|
|
114
|
-
|
|
115
|
-
### 🎮 **Entity-Based State**
|
|
116
|
-
|
|
117
|
-
Define behavior once, reuse it across all instances of the same type. Perfect for managing collections (todos, messages, cart items).
|
|
118
|
-
|
|
119
|
-
```javascript
|
|
120
|
-
// Define behavior for ALL todos
|
|
121
|
-
const todoType = {
|
|
122
|
-
toggle(todo, id) {
|
|
123
|
-
if (todo.id !== id) return
|
|
124
|
-
todo.completed = !todo.completed
|
|
125
|
-
},
|
|
98
|
+
// Define initial entities
|
|
99
|
+
const entities = {
|
|
100
|
+
work: { type: "todoList", todos: [] },
|
|
101
|
+
personal: { type: "todoList", todos: [] },
|
|
126
102
|
}
|
|
127
103
|
|
|
128
|
-
//
|
|
129
|
-
store
|
|
130
|
-
store.notify("toggle", "todo2")
|
|
104
|
+
// Create store
|
|
105
|
+
const store = createStore({ types, entities })
|
|
131
106
|
```
|
|
132
107
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
### 🔄 **Event Queue with Batching**
|
|
108
|
+
**Key differences:**
|
|
136
109
|
|
|
137
|
-
|
|
110
|
+
- ❌ No action creators
|
|
111
|
+
- ❌ No switch statements or cases
|
|
112
|
+
- ❌ No slice definitions with extraReducers
|
|
113
|
+
- ✅ Define what each entity type can do
|
|
114
|
+
- ✅ Add multiple instances by adding entities, not code
|
|
138
115
|
|
|
139
|
-
|
|
140
|
-
const store = createStore({ types, entities, mode: "batched" })
|
|
141
|
-
|
|
142
|
-
// Dispatch multiple events
|
|
143
|
-
store.notify("increment", "counter1")
|
|
144
|
-
store.notify("increment", "counter2")
|
|
145
|
-
store.notify("increment", "counter3")
|
|
116
|
+
---
|
|
146
117
|
|
|
147
|
-
|
|
148
|
-
store.update()
|
|
149
|
-
```
|
|
118
|
+
## Core Concepts
|
|
150
119
|
|
|
151
|
-
###
|
|
120
|
+
### 🎮 Entities and Types
|
|
152
121
|
|
|
153
|
-
|
|
122
|
+
State consists of **entities** (instances) that have a **type** (behavior definition). Think of type as a class and entities as instances:
|
|
154
123
|
|
|
155
124
|
```javascript
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
### 🌐 **Multiplayer-Ready**
|
|
125
|
+
const entities = {
|
|
126
|
+
workTodos: { type: "todoList", todos: [], priority: "high" },
|
|
127
|
+
personalTodos: { type: "todoList", todos: [], priority: "low" },
|
|
128
|
+
settings: { type: "settings", theme: "dark", language: "en" },
|
|
129
|
+
}
|
|
162
130
|
|
|
163
|
-
|
|
131
|
+
const types = {
|
|
132
|
+
todoList: {
|
|
133
|
+
addTodo(entity, text) {
|
|
134
|
+
entity.todos.push({ id: Date.now(), text })
|
|
135
|
+
},
|
|
136
|
+
toggle(entity, id) {
|
|
137
|
+
const todo = entity.todos.find((t) => t.id === id)
|
|
138
|
+
if (todo) todo.completed = !todo.completed
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
settings: {
|
|
142
|
+
setTheme(entity, theme) {
|
|
143
|
+
entity.theme = theme
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
```
|
|
164
148
|
|
|
165
|
-
|
|
166
|
-
// Start building solo
|
|
167
|
-
store.notify("addTodo", { text: "Buy milk" })
|
|
149
|
+
**Why this matters:**
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// States stay perfectly in sync across all clients
|
|
173
|
-
})
|
|
174
|
-
```
|
|
151
|
+
- Same behavior applies to all instances of that type
|
|
152
|
+
- No need to write separate code for each instance
|
|
153
|
+
- Your mental model matches your code structure
|
|
175
154
|
|
|
176
|
-
###
|
|
155
|
+
### 🔄 Event Handlers (Not Reducers)
|
|
177
156
|
|
|
178
|
-
|
|
157
|
+
Unlike Redux reducers, Inglorious Store uses **event handlers** that mutate directly. Immutability is guaranteed under the hood by Mutative (10x faster than Immer):
|
|
179
158
|
|
|
180
159
|
```javascript
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
160
|
+
const types = {
|
|
161
|
+
counter: {
|
|
162
|
+
increment(counter) {
|
|
163
|
+
counter.value++ // Looks like mutation, immutable in reality
|
|
164
|
+
},
|
|
185
165
|
},
|
|
186
166
|
}
|
|
187
167
|
```
|
|
188
168
|
|
|
189
|
-
### 🔗 **Redux-Compatible**
|
|
190
|
-
|
|
191
|
-
Works with `react-redux` and Redux DevTools. Provides both `notify()` and `dispatch()` for compatibility.
|
|
192
|
-
|
|
193
169
|
---
|
|
194
170
|
|
|
195
|
-
##
|
|
171
|
+
## Installation & Setup
|
|
196
172
|
|
|
197
|
-
###
|
|
173
|
+
### Basic Setup (React)
|
|
198
174
|
|
|
199
175
|
```javascript
|
|
200
176
|
import { createStore } from "@inglorious/store"
|
|
177
|
+
import { Provider, useSelector, useDispatch } from "react-redux"
|
|
201
178
|
|
|
202
|
-
//
|
|
179
|
+
// 1. Define entity types
|
|
203
180
|
const types = {
|
|
204
181
|
counter: {
|
|
205
182
|
increment(counter) {
|
|
@@ -211,674 +188,429 @@ const types = {
|
|
|
211
188
|
},
|
|
212
189
|
}
|
|
213
190
|
|
|
191
|
+
// 2. Define initial entities
|
|
214
192
|
const entities = {
|
|
215
193
|
counter1: { type: "counter", value: 0 },
|
|
216
|
-
counter2: { type: "counter", value: 10 },
|
|
217
194
|
}
|
|
218
195
|
|
|
196
|
+
// 3. Create store
|
|
219
197
|
const store = createStore({ types, entities })
|
|
220
198
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
199
|
+
// 4. Use with react-redux
|
|
200
|
+
function App() {
|
|
201
|
+
return (
|
|
202
|
+
<Provider store={store}>
|
|
203
|
+
<Counter />
|
|
204
|
+
</Provider>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
227
207
|
|
|
228
|
-
|
|
208
|
+
function Counter() {
|
|
209
|
+
const dispatch = useDispatch()
|
|
210
|
+
const count = useSelector((state) => state.counter1.value)
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div>
|
|
214
|
+
<p>{count}</p>
|
|
215
|
+
<button onClick={() => dispatch({ type: "increment" })}>+</button>
|
|
216
|
+
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
229
220
|
```
|
|
230
221
|
|
|
231
|
-
###
|
|
222
|
+
### With @inglorious/react-store (Recommended)
|
|
232
223
|
|
|
233
224
|
```javascript
|
|
234
|
-
import {
|
|
235
|
-
import { createSelector } from "@inglorious/store/select"
|
|
225
|
+
import { createReactStore } from "@inglorious/react-store"
|
|
236
226
|
|
|
237
|
-
|
|
238
|
-
const types = {
|
|
239
|
-
form: {
|
|
240
|
-
inputChange(entity, value) {
|
|
241
|
-
entity.value = value
|
|
242
|
-
},
|
|
243
|
-
formSubmit(entity) {
|
|
244
|
-
entity.value = ""
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
list: {
|
|
249
|
-
formSubmit(entity, value) {
|
|
250
|
-
entity.tasks.push({
|
|
251
|
-
id: entity.tasks.length + 1,
|
|
252
|
-
text: value,
|
|
253
|
-
completed: false,
|
|
254
|
-
})
|
|
255
|
-
},
|
|
256
|
-
toggleClick(entity, id) {
|
|
257
|
-
const task = entity.tasks.find((task) => task.id === id)
|
|
258
|
-
task.completed = !task.completed
|
|
259
|
-
},
|
|
260
|
-
deleteClick(entity, id) {
|
|
261
|
-
const index = entity.tasks.findIndex((task) => task.id === id)
|
|
262
|
-
entity.tasks.splice(index, 1)
|
|
263
|
-
},
|
|
264
|
-
clearClick(entity) {
|
|
265
|
-
entity.tasks = entity.tasks.filter((task) => !task.completed)
|
|
266
|
-
},
|
|
267
|
-
},
|
|
227
|
+
export const { Provider, useSelector, useNotify } = createReactStore(store)
|
|
268
228
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
229
|
+
function App() {
|
|
230
|
+
return (
|
|
231
|
+
<Provider>
|
|
232
|
+
<Counter />
|
|
233
|
+
</Provider>
|
|
234
|
+
) // No store prop needed
|
|
274
235
|
}
|
|
275
236
|
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
type: "footer",
|
|
288
|
-
activeFilter: "all",
|
|
289
|
-
},
|
|
237
|
+
function Counter() {
|
|
238
|
+
const notify = useNotify()
|
|
239
|
+
const count = useSelector((state) => state.counter1.value)
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
<p>{count}</p>
|
|
244
|
+
<button onClick={() => notify("increment")}>+</button> // cleaner API
|
|
245
|
+
<button onClick={() => notify("decrement")}>-</button>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
290
248
|
}
|
|
291
|
-
|
|
292
|
-
// 3. Create store
|
|
293
|
-
const store = createStore({ types, entities })
|
|
294
|
-
|
|
295
|
-
// 4. Create selectors
|
|
296
|
-
const selectTasks = (state) => state.list.tasks
|
|
297
|
-
const selectActiveFilter = (state) => state.footer.activeFilter
|
|
298
|
-
|
|
299
|
-
const selectFilteredTasks = createSelector(
|
|
300
|
-
[selectTasks, selectActiveFilter],
|
|
301
|
-
(tasks, activeFilter) => {
|
|
302
|
-
switch (activeFilter) {
|
|
303
|
-
case "active":
|
|
304
|
-
return tasks.filter((t) => !t.completed)
|
|
305
|
-
case "completed":
|
|
306
|
-
return tasks.filter((t) => t.completed)
|
|
307
|
-
default:
|
|
308
|
-
return tasks
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
// 5. Subscribe to changes
|
|
314
|
-
store.subscribe(() => {
|
|
315
|
-
console.log("Filtered tasks:", selectFilteredTasks(store.getState()))
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
// 6. Dispatch events (use notify or dispatch - both work!)
|
|
319
|
-
store.notify("inputChange", "Buy milk")
|
|
320
|
-
store.notify("formSubmit", store.getState().form.value)
|
|
321
|
-
store.notify("toggleClick", 1) // Only task with id=1 will respond
|
|
322
|
-
store.notify("filterClick", "active")
|
|
323
|
-
|
|
324
|
-
// 7. Process event queue (in eager mode this happens automatically)
|
|
325
|
-
store.update()
|
|
326
249
|
```
|
|
327
250
|
|
|
328
251
|
---
|
|
329
252
|
|
|
330
|
-
## Core
|
|
253
|
+
## Core Features
|
|
331
254
|
|
|
332
|
-
###
|
|
255
|
+
### 🎮 Entity-Based State
|
|
333
256
|
|
|
334
|
-
|
|
257
|
+
The real power: add entities dynamically without code changes.
|
|
335
258
|
|
|
336
|
-
|
|
259
|
+
**Redux/RTK:** To manage three todo lists, you can reuse a reducer, but you're still managing multiple slices manually:
|
|
337
260
|
|
|
338
261
|
```javascript
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
262
|
+
// Redux - manual management
|
|
263
|
+
const store = configureStore({
|
|
264
|
+
reducer: {
|
|
265
|
+
workTodos: todosReducer,
|
|
266
|
+
personalTodos: todosReducer,
|
|
267
|
+
shoppingTodos: todosReducer,
|
|
344
268
|
},
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// This broadcasts 'toggle' to all entities
|
|
348
|
-
store.notify("toggle", "todo1") // Only todo1 actually updates
|
|
269
|
+
})
|
|
349
270
|
```
|
|
350
271
|
|
|
351
|
-
**
|
|
352
|
-
|
|
353
|
-
- ✅ Multiple entities of different types can respond to the same event
|
|
354
|
-
- ✅ Enables reactive, decoupled behavior
|
|
355
|
-
- ✅ Perfect for coordinating related entities
|
|
356
|
-
- ✅ Natural fit for multiplayer/real-time sync
|
|
357
|
-
|
|
358
|
-
**Example of multiple entities responding:**
|
|
272
|
+
**Inglorious Store:** Same behavior, no duplication:
|
|
359
273
|
|
|
360
274
|
```javascript
|
|
361
275
|
const types = {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
enemy: {
|
|
368
|
-
gameOver(enemy) {
|
|
369
|
-
enemy.active = false
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
ui: {
|
|
373
|
-
gameOver(ui) {
|
|
374
|
-
ui.showGameOverScreen = true
|
|
276
|
+
todoList: {
|
|
277
|
+
addTodo(entity, text) {
|
|
278
|
+
entity.todos.push({ id: Date.now(), text })
|
|
375
279
|
},
|
|
376
280
|
},
|
|
377
281
|
}
|
|
378
282
|
|
|
379
|
-
|
|
380
|
-
|
|
283
|
+
const entities = {
|
|
284
|
+
work: { type: "todoList", todos: [] },
|
|
285
|
+
personal: { type: "todoList", todos: [] },
|
|
286
|
+
shopping: { type: "todoList", todos: [] },
|
|
287
|
+
}
|
|
381
288
|
```
|
|
382
289
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
Your state is a collection of **entities** (instances) organized by **type** (like classes or models).
|
|
290
|
+
**The kicker:** Add a new list at runtime:
|
|
386
291
|
|
|
387
292
|
```javascript
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
293
|
+
// Redux/RTK - would need to restructure the store
|
|
294
|
+
// Inglorious Store - just notify the built-in 'add' event
|
|
295
|
+
store.notify("add", { id: "archive", type: "todoList", todos: [] })
|
|
296
|
+
|
|
297
|
+
// This triggers the 'create' lifecycle event for the new entity
|
|
392
298
|
```
|
|
393
299
|
|
|
394
|
-
|
|
300
|
+
**Lifecycle events:**
|
|
395
301
|
|
|
396
|
-
|
|
302
|
+
Inglorious Store provides three built-in lifecycle events that are broadcast like any other event:
|
|
397
303
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
increment(counter) {
|
|
402
|
-
counter.value++
|
|
403
|
-
},
|
|
404
|
-
decrement(counter) {
|
|
405
|
-
counter.value--
|
|
406
|
-
},
|
|
407
|
-
}
|
|
304
|
+
- **`create`** - triggered when a new entity is added via the `add` event
|
|
305
|
+
- **`destroy`** - triggered when an entity is removed via the `remove` event
|
|
306
|
+
- **`morph`** - change an entity's type on the fly (like Redux's `replaceReducer` for individual entities)
|
|
408
307
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
308
|
+
Remember: events are broadcast to all entities. Each handler decides if it should respond:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
const types = {
|
|
312
|
+
todoList: {
|
|
313
|
+
// Every todoList receives the 'create' event, but only the new one should act on it
|
|
314
|
+
create(entity, id) {
|
|
315
|
+
if (entity.id !== id) return
|
|
316
|
+
entity.createdAt = Date.now()
|
|
414
317
|
},
|
|
415
|
-
|
|
416
|
-
|
|
318
|
+
|
|
319
|
+
destroy(entity, id) {
|
|
320
|
+
if (entity.id !== id) return
|
|
321
|
+
console.log(`Archived list: ${entity.id}`)
|
|
417
322
|
},
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
item.price = item.price * (1 - percent / 100)
|
|
323
|
+
|
|
324
|
+
addTodo(entity, text) {
|
|
325
|
+
entity.todos.push({ id: Date.now(), text })
|
|
422
326
|
},
|
|
423
327
|
},
|
|
424
|
-
|
|
328
|
+
}
|
|
425
329
|
```
|
|
426
330
|
|
|
427
|
-
###
|
|
331
|
+
### 🔊 Event Broadcasting
|
|
428
332
|
|
|
429
|
-
|
|
333
|
+
Events are broadcast to all entities via pub/sub. Every entity handler receives every event of that type. Each handler decides whether to respond based on the payload:
|
|
430
334
|
|
|
431
335
|
```javascript
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
336
|
+
const types = {
|
|
337
|
+
todoList: {
|
|
338
|
+
toggle(entity, id) {
|
|
339
|
+
// This runs for EVERY todoList entity, but only acts if it's the right one
|
|
340
|
+
if (entity.id !== id) return
|
|
341
|
+
const todo = entity.todos.find((t) => t.id === id)
|
|
342
|
+
if (todo) todo.completed = !todo.completed
|
|
343
|
+
},
|
|
436
344
|
},
|
|
437
345
|
}
|
|
438
346
|
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (!value.trim()) return // Validate first
|
|
443
|
-
behavior.formSubmit?.(entity, value, api)
|
|
444
|
-
},
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
// Function that adds loading state to async handlers
|
|
448
|
-
const withLoading = (behavior) => ({
|
|
449
|
-
async fetchData(entity, payload, api) {
|
|
450
|
-
entity.loading = true
|
|
451
|
-
try {
|
|
452
|
-
await behavior.fetchData?.(entity, payload, api)
|
|
453
|
-
} finally {
|
|
454
|
-
entity.loading = false
|
|
455
|
-
}
|
|
456
|
-
},
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
// Compose behaviors
|
|
460
|
-
const types = {
|
|
461
|
-
form: [baseHandlers, validated(baseHandlers), withLoading(baseHandlers)],
|
|
462
|
-
}
|
|
347
|
+
// Broadcast to all todo lists
|
|
348
|
+
store.notify("toggle", "todo1")
|
|
349
|
+
// Each list's toggle handler runs; only the one with todo1 actually updates
|
|
463
350
|
```
|
|
464
351
|
|
|
465
|
-
**
|
|
466
|
-
|
|
467
|
-
- ✅ Add cross-cutting concerns (validation, logging, error handling)
|
|
468
|
-
- ✅ Reuse behavior logic across entities
|
|
469
|
-
- ✅ Create middleware-like wrappers
|
|
470
|
-
- ✅ Cleaner than deep inheritance hierarchies
|
|
471
|
-
- ✅ Works for both apps and games
|
|
472
|
-
|
|
473
|
-
### Events
|
|
474
|
-
|
|
475
|
-
Events are broadcast to all relevant handlers in a pub/sub pattern.
|
|
352
|
+
**Multiple types responding to the same event:**
|
|
476
353
|
|
|
477
354
|
```javascript
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
355
|
+
const types = {
|
|
356
|
+
todoList: {
|
|
357
|
+
taskCompleted(entity, taskId) {
|
|
358
|
+
const task = entity.tasks.find((t) => t.id === taskId)
|
|
359
|
+
if (task) task.completed = true
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
stats: {
|
|
363
|
+
taskCompleted(entity, taskId) {
|
|
364
|
+
entity.completedCount++
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
notifications: {
|
|
368
|
+
taskCompleted(entity, taskId) {
|
|
369
|
+
entity.messages.push("Nice! Task completed.")
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}
|
|
486
373
|
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
store.update()
|
|
374
|
+
// One notify call, all three entity types respond
|
|
375
|
+
store.notify("taskCompleted", "task123")
|
|
490
376
|
```
|
|
491
377
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
### Systems (Optional)
|
|
378
|
+
In Redux, you handle this by making multiple reducers listen to the same action—it works by design, but you wire it up manually in each reducer. In RTK, you use `extraReducers` with `builder.addCase()`. In Inglorious Store, it's automatic: if a type has a handler for the event, it receives and processes it.
|
|
495
379
|
|
|
496
|
-
|
|
380
|
+
### ⚡ Async Operations
|
|
497
381
|
|
|
498
|
-
|
|
382
|
+
This is where the choice of "where does my logic live?" matters.
|
|
499
383
|
|
|
500
|
-
|
|
501
|
-
- Updates require looking at all entities together (not individually)
|
|
502
|
-
- Logic that can't be expressed as independent entity handlers
|
|
384
|
+
**Redux/RTK:** Should async logic live in a thunk (where it can access other slices) or in a reducer (where it's pure)? This is a design question you have to answer. Redux thunks are outside the reducer, so they're not deterministic. RTK's `createAsyncThunk` generates pending/fulfilled/rejected actions, spreading your logic across multiple places.
|
|
503
385
|
|
|
504
|
-
**
|
|
505
|
-
|
|
506
|
-
When adding an item to inventory, you need to check if the **total weight** of all items exceeds the limit. This can't be done in individual item handlers because each item only knows about itself.
|
|
386
|
+
**Inglorious Store:** Your event handlers can be async, and you get deterministic behavior automatically. Inside an async handler, you can access other parts of state (read-only), and you can trigger other events via `api.notify()`. Everything still maintains predictability because of the underlying event queue:
|
|
507
387
|
|
|
508
388
|
```javascript
|
|
509
389
|
const types = {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
390
|
+
todoList: {
|
|
391
|
+
async loadTodos(entity, payload, api) {
|
|
392
|
+
entity.loading = true
|
|
393
|
+
try {
|
|
394
|
+
const todos = await fetch("/api/todos").then((r) => r.json())
|
|
395
|
+
// Trigger another event—it goes in the queue and runs after this handler
|
|
396
|
+
api.notify("todosLoaded", todos)
|
|
397
|
+
} catch (error) {
|
|
398
|
+
api.notify("loadFailed", error.message)
|
|
399
|
+
}
|
|
514
400
|
},
|
|
515
|
-
},
|
|
516
|
-
}
|
|
517
401
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const items = Object.values(state).filter((e) => e.type === "item")
|
|
523
|
-
const currentWeight = items.reduce((sum, item) => sum + item.weight, 0)
|
|
524
|
-
const maxWeight = state.player.maxCarryWeight
|
|
525
|
-
|
|
526
|
-
// Check if adding this item would exceed the limit
|
|
527
|
-
if (currentWeight + newItemData.weight > maxWeight) {
|
|
528
|
-
// Reject the add - drop the heaviest item instead
|
|
529
|
-
const heaviestItem = items.reduce((max, item) =>
|
|
530
|
-
item.weight > max.weight ? item : max,
|
|
531
|
-
)
|
|
532
|
-
delete state[heaviestItem.id]
|
|
533
|
-
state.ui.message = `Dropped ${heaviestItem.name} (too heavy!)`
|
|
534
|
-
}
|
|
402
|
+
todosLoaded(entity, todos) {
|
|
403
|
+
entity.todos = todos
|
|
404
|
+
entity.loading = false
|
|
405
|
+
},
|
|
535
406
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
id: newId,
|
|
540
|
-
type: "item",
|
|
541
|
-
...newItemData,
|
|
542
|
-
}
|
|
407
|
+
loadFailed(entity, error) {
|
|
408
|
+
entity.error = error
|
|
409
|
+
entity.loading = false
|
|
543
410
|
},
|
|
544
411
|
},
|
|
545
|
-
|
|
412
|
+
}
|
|
546
413
|
```
|
|
547
414
|
|
|
548
|
-
|
|
415
|
+
Notice: you don't need pending/fulfilled/rejected actions. You control the flow directly. The `api` object passed to handlers provides:
|
|
416
|
+
|
|
417
|
+
- **`api.getEntities()`** - read entire state
|
|
418
|
+
- **`api.getEntity(id)`** - read one entity
|
|
419
|
+
- **`api.notify(type, payload)`** - trigger other events (queued, not immediate)
|
|
420
|
+
- **`api.getTypes()`** - access type definitions (mainly for middleware/plugins)
|
|
421
|
+
|
|
422
|
+
All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
|
|
549
423
|
|
|
550
|
-
|
|
551
|
-
- Must make a **coordinated decision** (which item to drop)
|
|
552
|
-
- Updates **multiple entities** based on aggregate state (delete one, add another)
|
|
553
|
-
- Can't be split into independent entity handlers
|
|
424
|
+
### 🌍 Systems for Global Logic
|
|
554
425
|
|
|
555
|
-
|
|
426
|
+
When you need to coordinate updates across multiple entities (not just respond to individual events), use systems. A system runs once per event and has write access to the entire state:
|
|
556
427
|
|
|
557
428
|
```javascript
|
|
558
429
|
const systems = [
|
|
559
430
|
{
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
// Find next player
|
|
570
|
-
const nextPlayerIndex =
|
|
571
|
-
(players.indexOf(currentPlayer) + 1) % players.length
|
|
572
|
-
const nextPlayer = players[nextPlayerIndex]
|
|
573
|
-
|
|
574
|
-
// Give turn to next player
|
|
575
|
-
nextPlayer.isTurn = true
|
|
576
|
-
nextPlayer.actionsRemaining = 3
|
|
577
|
-
|
|
578
|
-
// Update round counter if we've cycled through all players
|
|
579
|
-
if (nextPlayerIndex === 0) {
|
|
580
|
-
state.gameState.round++
|
|
581
|
-
}
|
|
431
|
+
taskCompleted(state, taskId) {
|
|
432
|
+
// Read from multiple todo lists
|
|
433
|
+
const allTodos = Object.values(state)
|
|
434
|
+
.filter((e) => e.type === "todoList")
|
|
435
|
+
.flatMap((e) => e.todos)
|
|
436
|
+
|
|
437
|
+
// Update global stats
|
|
438
|
+
state.stats.total = allTodos.length
|
|
439
|
+
state.stats.completed = allTodos.filter((t) => t.completed).length
|
|
582
440
|
},
|
|
583
441
|
},
|
|
584
442
|
]
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
**This requires a system because:**
|
|
588
|
-
|
|
589
|
-
- Must coordinate between multiple player entities
|
|
590
|
-
- Needs to maintain turn order across all players
|
|
591
|
-
- Updates multiple entities in a specific sequence
|
|
592
|
-
- Logic can't be split per-player
|
|
593
|
-
|
|
594
|
-
**For most apps, you won't need systems.** Use selectors for derived data and entity handlers for individual entity logic.
|
|
595
|
-
|
|
596
|
-
---
|
|
597
|
-
|
|
598
|
-
## API Reference
|
|
599
|
-
|
|
600
|
-
### `createStore(options)`
|
|
601
|
-
|
|
602
|
-
Creates a new store instance.
|
|
603
|
-
|
|
604
|
-
**Options:**
|
|
605
|
-
|
|
606
|
-
- `types` (object): Map of type names to behaviors (single object or array)
|
|
607
|
-
- `entities` (object): Initial entities by ID
|
|
608
|
-
- `systems` (array, optional): Global event handlers
|
|
609
|
-
- `middlewares` (array, optional): Middleware functions that enhance store behavior
|
|
610
|
-
- `mode` (`"eager"|"batched"`, optional): Whether `store.update()` is invoked automatically at every `store.notify()` or manually. Defaults to `"eager"`, which makes the store behave like Redux
|
|
611
|
-
|
|
612
|
-
**Returns:**
|
|
613
|
-
|
|
614
|
-
- `subscribe(listener)`: Subscribe to state changes
|
|
615
|
-
- `update(dt)`: Process event queue (optional `dt` for time-based logic)
|
|
616
|
-
- `notify(type, payload)`: Queue an event
|
|
617
|
-
- `dispatch(event)`: Redux-compatible event dispatch
|
|
618
|
-
- `getTypes()`: Returns the augmented types configuration
|
|
619
|
-
- `getState()`: Get current immutable state
|
|
620
|
-
- `setState(newState)`: Replace entire state
|
|
621
|
-
- `reset()`: Reset to initial state
|
|
622
|
-
|
|
623
|
-
### `createApi(store)`
|
|
624
|
-
|
|
625
|
-
Creates a convenience wrapper with utility methods.
|
|
626
|
-
|
|
627
|
-
**Returns:**
|
|
628
|
-
|
|
629
|
-
- `getTypes()`, `getEntities()`, `getEntity(id)`: State accessors
|
|
630
|
-
- `notify(type, payload)`: Dispatch events
|
|
631
|
-
|
|
632
|
-
### `createSelector(inputSelectors, resultFunc)`
|
|
633
443
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
```javascript
|
|
637
|
-
const selectCompletedTasks = createSelector(
|
|
638
|
-
[(state) => state.list.tasks],
|
|
639
|
-
(tasks) => tasks.filter((task) => task.completed),
|
|
640
|
-
)
|
|
444
|
+
const store = createStore({ types, entities, systems })
|
|
641
445
|
```
|
|
642
446
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
## Use Cases
|
|
447
|
+
Systems receive the entire state and can modify any entity. They're useful for cross-cutting concerns, maintaining derived state, or coordinating complex state updates that can't be expressed as individual entity handlers.
|
|
646
448
|
|
|
647
|
-
###
|
|
449
|
+
### 🔗 Behavior Composition
|
|
648
450
|
|
|
649
|
-
|
|
650
|
-
- **Apps that might need collaboration someday** (start simple, scale without refactoring)
|
|
651
|
-
- **Real-time collaboration** (like Figma, Notion, Google Docs)
|
|
652
|
-
- **Chat and messaging apps**
|
|
653
|
-
- **Live dashboards and monitoring**
|
|
654
|
-
- **Interactive data visualizations**
|
|
655
|
-
- **Apps with undo/redo**
|
|
656
|
-
- **Collection-based UIs** (lists, feeds, boards)
|
|
657
|
-
- **...and games!**
|
|
451
|
+
A type can be a single behavior object, or an array of behaviors. A behavior is either an object with event handlers, or a function that takes a behavior and returns an enhanced behavior (decorator pattern):
|
|
658
452
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
---
|
|
668
|
-
|
|
669
|
-
## Comparison
|
|
670
|
-
|
|
671
|
-
| Feature | Inglorious Store | Redux | Redux Toolkit | Zustand | Jotai | Pinia | MobX |
|
|
672
|
-
| --------------------------- | ----------------- | ---------------- | ---------------- | ------------- | ------------- | --------------- | --------------- |
|
|
673
|
-
| **Integrated Immutability** | ✅ Mutative | ❌ Manual | ✅ Immer | ❌ Manual | ✅ Optional | ✅ Built-in | ✅ Observables |
|
|
674
|
-
| **Event Queue/Batching** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ Automatic |
|
|
675
|
-
| **Dispatch from Handlers** | ✅ Safe (queued) | ❌ Not allowed | ❌ Not allowed | ✅ | ✅ | ✅ | ✅ |
|
|
676
|
-
| **Redux DevTools** | ⚠️ Limited | ✅ Native | ✅ Native | ✅ Middleware | ⚠️ Limited | ✅ Vue DevTools | ⚠️ Limited |
|
|
677
|
-
| **react-redux Compatible** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ | ❌ | ❌ Vue only | ❌ |
|
|
678
|
-
| **Time-Travel Debug** | ✅ Built-in | ✅ Via DevTools | ✅ Via DevTools | ⚠️ Manual | ❌ | ⚠️ Limited | ❌ |
|
|
679
|
-
| **Entity-Based State** | ✅ First-class | ⚠️ Manual | ✅ EntityAdapter | ❌ | ❌ | ❌ | ❌ |
|
|
680
|
-
| **Pub/Sub Events** | ✅ Core pattern | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
681
|
-
| **Multiplayer-Ready** | ✅ Deterministic | ⚠️ With work | ⚠️ With work | ⚠️ With work | ❌ | ❌ | ❌ |
|
|
682
|
-
| **Testability** | ✅ Pure functions | ✅ Pure reducers | ✅ Pure reducers | ⚠️ With mocks | ⚠️ With mocks | ⚠️ With mocks | ❌ Side effects |
|
|
683
|
-
| **Learning Curve** | Medium | High | Medium | Low | Medium | Low | Medium |
|
|
684
|
-
| **Bundle Size** | Small | Small | Medium | Tiny | Small | Medium | Medium |
|
|
685
|
-
|
|
686
|
-
### Key Differences
|
|
453
|
+
```javascript
|
|
454
|
+
// Base behavior
|
|
455
|
+
const handlers = {
|
|
456
|
+
submit(entity, value) {
|
|
457
|
+
entity.value = ""
|
|
458
|
+
},
|
|
459
|
+
}
|
|
687
460
|
|
|
688
|
-
|
|
461
|
+
// Function that wraps and enhances a behavior
|
|
462
|
+
const validated = (behavior) => ({
|
|
463
|
+
submit(entity, value, api) {
|
|
464
|
+
if (!value.trim()) return
|
|
465
|
+
behavior.submit?.(entity, value, api)
|
|
466
|
+
},
|
|
467
|
+
})
|
|
689
468
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
469
|
+
// Another wrapper
|
|
470
|
+
const withLoading = (behavior) => ({
|
|
471
|
+
submit(entity, value, api) {
|
|
472
|
+
entity.loading = true
|
|
473
|
+
behavior.submit?.(entity, value, api)
|
|
474
|
+
entity.loading = false
|
|
475
|
+
},
|
|
476
|
+
})
|
|
695
477
|
|
|
696
|
-
|
|
478
|
+
// Compose them together
|
|
479
|
+
const types = {
|
|
480
|
+
form: [handlers, validated, withLoading],
|
|
481
|
+
}
|
|
482
|
+
```
|
|
697
483
|
|
|
698
|
-
|
|
699
|
-
- Built-in time-travel debugging
|
|
700
|
-
- Entity/type architecture for collections
|
|
701
|
-
- Event queue prevents cascading updates
|
|
702
|
-
- Redux DevTools compatible
|
|
484
|
+
When multiple behaviors define the same event, they all run in order. This allows you to build middleware-like patterns: validation, logging, error handling, loading states, etc.
|
|
703
485
|
|
|
704
|
-
|
|
486
|
+
### ⏱️ Batched Mode
|
|
705
487
|
|
|
706
|
-
|
|
707
|
-
- Better for entity collections
|
|
708
|
-
- Built-in normalization
|
|
709
|
-
- Explicit event flow
|
|
488
|
+
Process multiple events together before re-rendering:
|
|
710
489
|
|
|
711
|
-
|
|
490
|
+
```javascript
|
|
491
|
+
const store = createStore({ types, entities, mode: "batched" })
|
|
712
492
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
493
|
+
store.notify("playerMoved", { x: 100, y: 50 })
|
|
494
|
+
store.notify("enemyAttacked", { damage: 10 })
|
|
495
|
+
store.notify("particleCreated", { type: "explosion" })
|
|
716
496
|
|
|
717
|
-
|
|
497
|
+
requestAnimationFrame(() => store.update())
|
|
498
|
+
```
|
|
718
499
|
|
|
719
|
-
-
|
|
720
|
-
- Serializable state (easier persistence/sync)
|
|
721
|
-
- Deterministic (better for debugging)
|
|
722
|
-
- Redux DevTools compatible
|
|
500
|
+
Instead of re-rendering after each event, batch them and re-render once. This is what powers high-performance game engines and smooth animations.
|
|
723
501
|
|
|
724
502
|
---
|
|
725
503
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
- Building real-time/collaborative features
|
|
729
|
-
- Managing collections of similar items
|
|
730
|
-
- Need deterministic state for multiplayer
|
|
731
|
-
- Want built-in time-travel debugging
|
|
732
|
-
- Coming from Redux and want better DX
|
|
733
|
-
|
|
734
|
-
**When to choose alternatives:**
|
|
504
|
+
## Comparison with Other State Libraries
|
|
735
505
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
506
|
+
| Feature | Redux | RTK | Zustand | Jotai | Pinia | MobX | Inglorious Store |
|
|
507
|
+
| ------------------------- | ------------ | ------------ | ---------- | ---------- | ---------- | ---------- | ---------------- |
|
|
508
|
+
| **Boilerplate** | 🔴 High | 🟡 Medium | 🟢 Low | 🟢 Low | 🟡 Medium | 🟢 Low | 🟢 Low |
|
|
509
|
+
| **Multiple instances** | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟡 Medium | 🟡 Medium | 🟢 Built-in |
|
|
510
|
+
| **Lifecycle events** | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🟢 Yes |
|
|
511
|
+
| **Async logic placement** | 🟡 Thunks | 🟡 Complex | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 In handlers |
|
|
512
|
+
| **Redux DevTools** | 🟢 Yes | 🟢 Yes | 🟡 Partial | 🟡 Partial | 🟡 Partial | 🟢 Yes | 🟢 Yes |
|
|
513
|
+
| **Time-travel debugging** | 🟢 Yes | 🟢 Yes | 🔴 No | 🔴 No | 🔴 No | 🟡 Limited | 🟢 Yes |
|
|
514
|
+
| **Testability** | 🟢 Excellent | 🟢 Excellent | 🟡 Good | 🟡 Good | 🟡 Good | 🟡 Medium | 🟢 Excellent |
|
|
515
|
+
| **Immutability** | 🔴 Manual | 🟢 Immer | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟢 Mutative |
|
|
740
516
|
|
|
741
517
|
---
|
|
742
518
|
|
|
743
|
-
##
|
|
744
|
-
|
|
745
|
-
Adding multiplayer to an existing app is usually a massive refactor. With Inglorious Store, it's an afternoon project.
|
|
746
|
-
|
|
747
|
-
### Step 1: Your app already works locally
|
|
748
|
-
|
|
749
|
-
```javascript
|
|
750
|
-
store.notify("movePlayer", { x: 10, y: 20 })
|
|
751
|
-
store.update()
|
|
752
|
-
```
|
|
519
|
+
## API Reference
|
|
753
520
|
|
|
754
|
-
###
|
|
521
|
+
### `createStore(options)`
|
|
755
522
|
|
|
756
523
|
```javascript
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// Send your events to other clients
|
|
763
|
-
const processedEvents = store.update()
|
|
764
|
-
processedEvents.forEach((event) => {
|
|
765
|
-
socket.emit("event", event)
|
|
524
|
+
const store = createStore({
|
|
525
|
+
types, // Object: entity type definitions
|
|
526
|
+
entities, // Object: initial entities
|
|
527
|
+
systems, // Array (optional): global state handlers
|
|
528
|
+
mode, // String (optional): 'eager' (default) or 'batched'
|
|
766
529
|
})
|
|
767
530
|
```
|
|
768
531
|
|
|
769
|
-
**
|
|
770
|
-
|
|
771
|
-
### Why This Works
|
|
772
|
-
|
|
773
|
-
1. **Deterministic:** Same events + same state = same result (always)
|
|
774
|
-
2. **Serializable:** Events are plain objects (easy to send over network)
|
|
775
|
-
3. **Ordered:** Event queue ensures predictable processing
|
|
776
|
-
4. **Conflict-free:** Last write wins, or implement custom merge logic
|
|
777
|
-
|
|
778
|
-
### Example: Collaborative Todo List
|
|
779
|
-
|
|
780
|
-
```javascript
|
|
781
|
-
// Client A adds a todo
|
|
782
|
-
store.notify("addTodo", { id: "todo1", text: "Buy milk" })
|
|
783
|
-
|
|
784
|
-
// Event gets broadcast to all clients
|
|
785
|
-
// All clients process the same event
|
|
786
|
-
// All clients end up with identical state
|
|
787
|
-
|
|
788
|
-
// Even works offline! Events queue up, sync when reconnected
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
This is exactly how multiplayer games work. Now your app can too.
|
|
792
|
-
|
|
793
|
-
---
|
|
794
|
-
|
|
795
|
-
## Advanced: Time-Based Updates
|
|
532
|
+
**Returns:** Redux-compatible store
|
|
796
533
|
|
|
797
|
-
|
|
534
|
+
### Types Definition
|
|
798
535
|
|
|
799
536
|
```javascript
|
|
800
537
|
const types = {
|
|
801
|
-
|
|
538
|
+
entityType: [
|
|
539
|
+
// Behavior objects
|
|
802
540
|
{
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
particle.y += particle.velocityY * dt
|
|
807
|
-
particle.life -= dt
|
|
541
|
+
eventName(entity, payload, api) {
|
|
542
|
+
entity.value = payload
|
|
543
|
+
api.notify("otherEvent", data)
|
|
808
544
|
},
|
|
809
545
|
},
|
|
546
|
+
// Behavior functions (decorators)
|
|
547
|
+
(behavior) => ({
|
|
548
|
+
eventName(entity, payload, api) {
|
|
549
|
+
// Wrap the behavior
|
|
550
|
+
behavior.eventName?.(entity, payload, api)
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
810
553
|
],
|
|
811
554
|
}
|
|
812
|
-
|
|
813
|
-
// Run at 30 FPS (good for most UIs)
|
|
814
|
-
setInterval(() => store.update(), 1000 / 30)
|
|
815
|
-
|
|
816
|
-
// Or 60 FPS (for smooth animations/games)
|
|
817
|
-
function loop() {
|
|
818
|
-
store.update()
|
|
819
|
-
requestAnimationFrame(loop)
|
|
820
|
-
}
|
|
821
|
-
loop()
|
|
822
555
|
```
|
|
823
556
|
|
|
824
|
-
|
|
557
|
+
### Event Handler API
|
|
825
558
|
|
|
826
|
-
|
|
559
|
+
Each handler receives three arguments:
|
|
827
560
|
|
|
828
|
-
|
|
561
|
+
- **`entity`** - the entity instance (mutate freely, immutability guaranteed)
|
|
562
|
+
- **`payload`** - data passed with the event
|
|
563
|
+
- **`api`** - access to store methods:
|
|
564
|
+
- `getEntities()` - entire state (read-only)
|
|
565
|
+
- `getEntity(id)` - single entity (read-only)
|
|
566
|
+
- `notify(type, payload)` - trigger other events (queued)
|
|
567
|
+
- `getTypes()` - type definitions (for middleware)
|
|
829
568
|
|
|
830
|
-
|
|
569
|
+
### Built-in Lifecycle Events
|
|
831
570
|
|
|
832
|
-
|
|
571
|
+
- **`create(entity, id)`** - triggered when entity added via `add` event
|
|
572
|
+
- **`destroy(entity, id)`** - triggered when entity removed via `remove` event
|
|
573
|
+
- **`morph(entity, newType)`** - triggered when entity type changes
|
|
833
574
|
|
|
834
|
-
|
|
835
|
-
store.notify("addTodo", { text: "Buy milk" })
|
|
836
|
-
```
|
|
575
|
+
### Notify vs Dispatch
|
|
837
576
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
### Month 6: Users love it, ask for undo/redo
|
|
577
|
+
Both work (dispatch for Redux compatibility), but `notify` is cleaner:
|
|
841
578
|
|
|
842
579
|
```javascript
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
store.setState(snapshot) // Undo!
|
|
580
|
+
store.notify("eventName", payload)
|
|
581
|
+
store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
|
|
846
582
|
```
|
|
847
583
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
### Year 1: Competitor launches with real-time collaboration
|
|
584
|
+
---
|
|
851
585
|
|
|
852
|
-
|
|
853
|
-
socket.on("remote-event", (e) => store.notify(e.type, e.payload))
|
|
854
|
-
```
|
|
586
|
+
## Use Cases
|
|
855
587
|
|
|
856
|
-
|
|
588
|
+
### Perfect For
|
|
857
589
|
|
|
858
|
-
|
|
590
|
+
- 🎮 Apps with multiple instances of the same entity type
|
|
591
|
+
- 🎯 Real-time collaborative features
|
|
592
|
+
- ⚡ Complex state coordination and async operations
|
|
593
|
+
- 📊 High-frequency updates (animations, games)
|
|
594
|
+
- 🔄 Undo/redo, time-travel debugging
|
|
859
595
|
|
|
860
|
-
|
|
596
|
+
### Still Great For
|
|
861
597
|
|
|
862
|
-
|
|
598
|
+
- Any Redux use case (true drop-in replacement)
|
|
599
|
+
- Migration path from Redux (keep using react-redux)
|
|
863
600
|
|
|
864
601
|
---
|
|
865
602
|
|
|
866
|
-
##
|
|
603
|
+
## Part of the Inglorious Engine
|
|
867
604
|
|
|
868
|
-
|
|
869
|
-
- 🎮 **[@inglorious/engine](https://github.com/IngloriousCoderz/inglorious-engine)** - Full game engine built on this store
|
|
870
|
-
- 🌐 **[@inglorious/server](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/server)** - Server-side multiplayer support
|
|
871
|
-
- 💬 **[GitHub Discussions](https://github.com/IngloriousCoderz/inglorious-engine/discussions)** - Get help and share what you're building
|
|
605
|
+
This store powers the [Inglorious Engine](https://github.com/IngloriousCoderz/inglorious-engine), a functional game engine. The same patterns that power games power your web apps.
|
|
872
606
|
|
|
873
607
|
---
|
|
874
608
|
|
|
875
609
|
## License
|
|
876
610
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
611
|
+
MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
880
612
|
|
|
881
|
-
|
|
613
|
+
Free to use, modify, and distribute.
|
|
882
614
|
|
|
883
615
|
---
|
|
884
616
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
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",
|
package/src/api.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
export function createApi(store, extras) {
|
|
2
|
-
const getTypes = () => store.getTypes()
|
|
3
|
-
|
|
4
|
-
const getEntities = () => store.getState()
|
|
5
|
-
|
|
6
|
-
const getEntity = (id) => getEntities()[id]
|
|
7
|
-
|
|
8
2
|
return {
|
|
9
|
-
getTypes,
|
|
10
|
-
getEntities,
|
|
11
|
-
getEntity,
|
|
3
|
+
getTypes: store.getTypes,
|
|
4
|
+
getEntities: store.getState,
|
|
5
|
+
getEntity: (id) => store.getState()[id],
|
|
12
6
|
dispatch: store.dispatch,
|
|
13
7
|
notify: store.notify,
|
|
14
8
|
...extras,
|
package/src/client/dev-tools.js
CHANGED
|
@@ -16,6 +16,16 @@ export function connectDevTools(store, config = {}) {
|
|
|
16
16
|
const name = config.name ?? document.title
|
|
17
17
|
const skippedEvents = config.skippedEvents ?? []
|
|
18
18
|
|
|
19
|
+
if (config.mode !== "batched") {
|
|
20
|
+
const baseDispatch = store.dispatch
|
|
21
|
+
store.dispatch = (action) => {
|
|
22
|
+
baseDispatch(action)
|
|
23
|
+
if (!skippedEvents.includes(action.type)) {
|
|
24
|
+
sendAction(action, store.getState())
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
devToolsInstance = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
|
|
20
30
|
name,
|
|
21
31
|
predicate: (state, action) => !skippedEvents.includes(action.type),
|
package/src/store.js
CHANGED
|
@@ -45,6 +45,7 @@ export function createStore({
|
|
|
45
45
|
? applyMiddlewares(...middlewares)(baseStore)
|
|
46
46
|
: baseStore
|
|
47
47
|
const api = createApi(store, store.extras)
|
|
48
|
+
store._api = api
|
|
48
49
|
return store
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -135,7 +136,8 @@ export function createStore({
|
|
|
135
136
|
* @param {any} payload - The event object payload to notify.
|
|
136
137
|
*/
|
|
137
138
|
function notify(type, payload) {
|
|
138
|
-
dispatch
|
|
139
|
+
// NOTE: it's important to invoke store.dispatch instead of dispatch, otherwise we cannot override it
|
|
140
|
+
store.dispatch({ type, payload })
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
/**
|