@inglorious/store 5.4.1 → 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 +385 -608
- package/package.json +1 -1
- package/src/api.js +3 -17
- package/src/client/dev-tools.js +10 -0
- package/src/store.js +3 -1
- package/src/types.js +3 -2
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
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**For React apps**, also install the React bindings:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm install @inglorious/react-store
|
|
32
|
+
npm install @inglorious/store react-redux
|
|
44
33
|
```
|
|
45
34
|
|
|
46
|
-
|
|
35
|
+
**For React:** Works with standard `react-redux` without any extra packages.
|
|
47
36
|
|
|
48
37
|
---
|
|
49
38
|
|
|
50
|
-
##
|
|
39
|
+
## Quick Comparison: Redux vs RTK vs Inglorious Store
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
### Eager Mode (default) - Like Redux
|
|
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
|
-
|
|
108
|
+
**Key differences:**
|
|
134
109
|
|
|
135
|
-
|
|
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
|
|
136
115
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
```javascript
|
|
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,629 +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"
|
|
236
|
-
|
|
237
|
-
// 1. Define types (can be a single behavior or array of behaviors)
|
|
238
|
-
const types = {
|
|
239
|
-
form: {
|
|
240
|
-
inputChange(entity, value) {
|
|
241
|
-
entity.value = value
|
|
242
|
-
},
|
|
243
|
-
formSubmit(entity) {
|
|
244
|
-
entity.value = ""
|
|
245
|
-
},
|
|
246
|
-
},
|
|
225
|
+
import { createReactStore } from "@inglorious/react-store"
|
|
247
226
|
|
|
248
|
-
|
|
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
|
}
|
|
249
|
+
```
|
|
291
250
|
|
|
292
|
-
|
|
293
|
-
const store = createStore({ types, entities })
|
|
251
|
+
---
|
|
294
252
|
|
|
295
|
-
|
|
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
|
-
)
|
|
253
|
+
## Core Features
|
|
312
254
|
|
|
313
|
-
|
|
314
|
-
store.subscribe(() => {
|
|
315
|
-
console.log("Filtered tasks:", selectFilteredTasks(store.getState()))
|
|
316
|
-
})
|
|
255
|
+
### 🎮 Entity-Based State
|
|
317
256
|
|
|
318
|
-
|
|
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")
|
|
257
|
+
The real power: add entities dynamically without code changes.
|
|
323
258
|
|
|
324
|
-
|
|
325
|
-
store.update()
|
|
326
|
-
```
|
|
259
|
+
**Redux/RTK:** To manage three todo lists, you can reuse a reducer, but you're still managing multiple slices manually:
|
|
327
260
|
|
|
328
|
-
|
|
261
|
+
```javascript
|
|
262
|
+
// Redux - manual management
|
|
263
|
+
const store = configureStore({
|
|
264
|
+
reducer: {
|
|
265
|
+
workTodos: todosReducer,
|
|
266
|
+
personalTodos: todosReducer,
|
|
267
|
+
shoppingTodos: todosReducer,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
```
|
|
329
271
|
|
|
330
|
-
|
|
272
|
+
**Inglorious Store:** Same behavior, no duplication:
|
|
331
273
|
|
|
332
|
-
|
|
274
|
+
```javascript
|
|
275
|
+
const types = {
|
|
276
|
+
todoList: {
|
|
277
|
+
addTodo(entity, text) {
|
|
278
|
+
entity.todos.push({ id: Date.now(), text })
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
}
|
|
333
282
|
|
|
334
|
-
|
|
283
|
+
const entities = {
|
|
284
|
+
work: { type: "todoList", todos: [] },
|
|
285
|
+
personal: { type: "todoList", todos: [] },
|
|
286
|
+
shopping: { type: "todoList", todos: [] },
|
|
287
|
+
}
|
|
288
|
+
```
|
|
335
289
|
|
|
336
|
-
|
|
290
|
+
**The kicker:** Add a new list at runtime:
|
|
337
291
|
|
|
338
292
|
```javascript
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (todo.id !== id) return // Filter: only this todo responds
|
|
343
|
-
todo.completed = !todo.completed
|
|
344
|
-
},
|
|
345
|
-
}
|
|
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: [] })
|
|
346
296
|
|
|
347
|
-
// This
|
|
348
|
-
store.notify("toggle", "todo1") // Only todo1 actually updates
|
|
297
|
+
// This triggers the 'create' lifecycle event for the new entity
|
|
349
298
|
```
|
|
350
299
|
|
|
351
|
-
**
|
|
300
|
+
**Lifecycle events:**
|
|
352
301
|
|
|
353
|
-
|
|
354
|
-
- ✅ Enables reactive, decoupled behavior
|
|
355
|
-
- ✅ Perfect for coordinating related entities
|
|
356
|
-
- ✅ Natural fit for multiplayer/real-time sync
|
|
302
|
+
Inglorious Store provides three built-in lifecycle events that are broadcast like any other event:
|
|
357
303
|
|
|
358
|
-
|
|
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)
|
|
307
|
+
|
|
308
|
+
Remember: events are broadcast to all entities. Each handler decides if it should respond:
|
|
359
309
|
|
|
360
310
|
```javascript
|
|
361
311
|
const types = {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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()
|
|
365
317
|
},
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
318
|
+
|
|
319
|
+
destroy(entity, id) {
|
|
320
|
+
if (entity.id !== id) return
|
|
321
|
+
console.log(`Archived list: ${entity.id}`)
|
|
370
322
|
},
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
ui.showGameOverScreen = true
|
|
323
|
+
|
|
324
|
+
addTodo(entity, text) {
|
|
325
|
+
entity.todos.push({ id: Date.now(), text })
|
|
375
326
|
},
|
|
376
327
|
},
|
|
377
328
|
}
|
|
378
|
-
|
|
379
|
-
// One event, all three entity types respond (if they have the handler)
|
|
380
|
-
store.notify("gameOver")
|
|
381
329
|
```
|
|
382
330
|
|
|
383
|
-
###
|
|
331
|
+
### 🔊 Event Broadcasting
|
|
384
332
|
|
|
385
|
-
|
|
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:
|
|
386
334
|
|
|
387
335
|
```javascript
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
},
|
|
344
|
+
},
|
|
391
345
|
}
|
|
392
|
-
```
|
|
393
346
|
|
|
394
|
-
|
|
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
|
|
350
|
+
```
|
|
395
351
|
|
|
396
|
-
|
|
352
|
+
**Multiple types responding to the same event:**
|
|
397
353
|
|
|
398
354
|
```javascript
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
decrement(counter) {
|
|
405
|
-
counter.value--
|
|
406
|
-
},
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Array of behaviors (composable)
|
|
410
|
-
const cartItemType = [
|
|
411
|
-
{
|
|
412
|
-
incrementQuantity(item) {
|
|
413
|
-
item.quantity++
|
|
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
|
|
414
360
|
},
|
|
415
|
-
|
|
416
|
-
|
|
361
|
+
},
|
|
362
|
+
stats: {
|
|
363
|
+
taskCompleted(entity, taskId) {
|
|
364
|
+
entity.completedCount++
|
|
417
365
|
},
|
|
418
366
|
},
|
|
419
|
-
{
|
|
420
|
-
|
|
421
|
-
|
|
367
|
+
notifications: {
|
|
368
|
+
taskCompleted(entity, taskId) {
|
|
369
|
+
entity.messages.push("Nice! Task completed.")
|
|
422
370
|
},
|
|
423
371
|
},
|
|
424
|
-
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Events
|
|
428
|
-
|
|
429
|
-
Events are broadcast to all relevant handlers in a pub/sub pattern.
|
|
430
|
-
|
|
431
|
-
```javascript
|
|
432
|
-
// Simplest form - just the entity ID
|
|
433
|
-
store.notify("increment", "counter1")
|
|
434
|
-
|
|
435
|
-
// With additional data
|
|
436
|
-
store.notify("applyDiscount", { id: "item1", percent: 10 })
|
|
437
|
-
|
|
438
|
-
// Also supports dispatch() for Redux compatibility
|
|
439
|
-
store.dispatch({ type: "increment", payload: "counter1" })
|
|
372
|
+
}
|
|
440
373
|
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
store.update()
|
|
374
|
+
// One notify call, all three entity types respond
|
|
375
|
+
store.notify("taskCompleted", "task123")
|
|
444
376
|
```
|
|
445
377
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
### 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.
|
|
449
379
|
|
|
450
|
-
|
|
380
|
+
### ⚡ Async Operations
|
|
451
381
|
|
|
452
|
-
|
|
382
|
+
This is where the choice of "where does my logic live?" matters.
|
|
453
383
|
|
|
454
|
-
|
|
455
|
-
- Updates require looking at all entities together (not individually)
|
|
456
|
-
- 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.
|
|
457
385
|
|
|
458
|
-
**
|
|
459
|
-
|
|
460
|
-
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:
|
|
461
387
|
|
|
462
388
|
```javascript
|
|
463
389
|
const types = {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
}
|
|
468
400
|
},
|
|
469
|
-
},
|
|
470
|
-
}
|
|
471
401
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const items = Object.values(state).filter((e) => e.type === "item")
|
|
477
|
-
const currentWeight = items.reduce((sum, item) => sum + item.weight, 0)
|
|
478
|
-
const maxWeight = state.player.maxCarryWeight
|
|
479
|
-
|
|
480
|
-
// Check if adding this item would exceed the limit
|
|
481
|
-
if (currentWeight + newItemData.weight > maxWeight) {
|
|
482
|
-
// Reject the add - drop the heaviest item instead
|
|
483
|
-
const heaviestItem = items.reduce((max, item) =>
|
|
484
|
-
item.weight > max.weight ? item : max,
|
|
485
|
-
)
|
|
486
|
-
delete state[heaviestItem.id]
|
|
487
|
-
state.ui.message = `Dropped ${heaviestItem.name} (too heavy!)`
|
|
488
|
-
}
|
|
402
|
+
todosLoaded(entity, todos) {
|
|
403
|
+
entity.todos = todos
|
|
404
|
+
entity.loading = false
|
|
405
|
+
},
|
|
489
406
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
id: newId,
|
|
494
|
-
type: "item",
|
|
495
|
-
...newItemData,
|
|
496
|
-
}
|
|
407
|
+
loadFailed(entity, error) {
|
|
408
|
+
entity.error = error
|
|
409
|
+
entity.loading = false
|
|
497
410
|
},
|
|
498
411
|
},
|
|
499
|
-
|
|
412
|
+
}
|
|
500
413
|
```
|
|
501
414
|
|
|
502
|
-
|
|
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)
|
|
503
421
|
|
|
504
|
-
|
|
505
|
-
- Must make a **coordinated decision** (which item to drop)
|
|
506
|
-
- Updates **multiple entities** based on aggregate state (delete one, add another)
|
|
507
|
-
- Can't be split into independent entity handlers
|
|
422
|
+
All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
|
|
508
423
|
|
|
509
|
-
|
|
424
|
+
### 🌍 Systems for Global Logic
|
|
425
|
+
|
|
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:
|
|
510
427
|
|
|
511
428
|
```javascript
|
|
512
429
|
const systems = [
|
|
513
430
|
{
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
// Find next player
|
|
524
|
-
const nextPlayerIndex =
|
|
525
|
-
(players.indexOf(currentPlayer) + 1) % players.length
|
|
526
|
-
const nextPlayer = players[nextPlayerIndex]
|
|
527
|
-
|
|
528
|
-
// Give turn to next player
|
|
529
|
-
nextPlayer.isTurn = true
|
|
530
|
-
nextPlayer.actionsRemaining = 3
|
|
531
|
-
|
|
532
|
-
// Update round counter if we've cycled through all players
|
|
533
|
-
if (nextPlayerIndex === 0) {
|
|
534
|
-
state.gameState.round++
|
|
535
|
-
}
|
|
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
|
|
536
440
|
},
|
|
537
441
|
},
|
|
538
442
|
]
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
**This requires a system because:**
|
|
542
|
-
|
|
543
|
-
- Must coordinate between multiple player entities
|
|
544
|
-
- Needs to maintain turn order across all players
|
|
545
|
-
- Updates multiple entities in a specific sequence
|
|
546
|
-
- Logic can't be split per-player
|
|
547
|
-
|
|
548
|
-
**For most apps, you won't need systems.** Use selectors for derived data and entity handlers for individual entity logic.
|
|
549
|
-
|
|
550
|
-
---
|
|
551
443
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
### `createStore(options)`
|
|
555
|
-
|
|
556
|
-
Creates a new store instance.
|
|
557
|
-
|
|
558
|
-
**Options:**
|
|
559
|
-
|
|
560
|
-
- `types` (object): Map of type names to behaviors (single object or array)
|
|
561
|
-
- `entities` (object): Initial entities by ID
|
|
562
|
-
- `systems` (array, optional): Global event handlers
|
|
563
|
-
- `middlewares` (array, optional): Middleware functions that enhance store behavior
|
|
564
|
-
- `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
|
|
565
|
-
|
|
566
|
-
**Returns:**
|
|
567
|
-
|
|
568
|
-
- `subscribe(listener)`: Subscribe to state changes
|
|
569
|
-
- `update(dt)`: Process event queue (optional `dt` for time-based logic)
|
|
570
|
-
- `notify(type, payload)`: Queue an event
|
|
571
|
-
- `dispatch(event)`: Redux-compatible event dispatch
|
|
572
|
-
- `getTypes()`: Returns the augmented types configuration
|
|
573
|
-
- `getState()`: Get current immutable state
|
|
574
|
-
- `setState(newState)`: Replace entire state
|
|
575
|
-
- `reset()`: Reset to initial state
|
|
576
|
-
|
|
577
|
-
### `createApi(store)`
|
|
578
|
-
|
|
579
|
-
Creates a convenience wrapper with utility methods.
|
|
580
|
-
|
|
581
|
-
**Returns:**
|
|
582
|
-
|
|
583
|
-
- `createSelector(inputSelectors, resultFunc)`: Memoized selectors
|
|
584
|
-
- `getTypes()`, `getEntities()`, `getEntity(id)`: State accessors
|
|
585
|
-
- `notify(type, payload)`: Dispatch events
|
|
586
|
-
|
|
587
|
-
### `createSelector(inputSelectors, resultFunc)`
|
|
588
|
-
|
|
589
|
-
Create memoized, performant selectors.
|
|
590
|
-
|
|
591
|
-
```javascript
|
|
592
|
-
const selectCompletedTasks = createSelector(
|
|
593
|
-
[(state) => state.list.tasks],
|
|
594
|
-
(tasks) => tasks.filter((task) => task.completed),
|
|
595
|
-
)
|
|
444
|
+
const store = createStore({ types, entities, systems })
|
|
596
445
|
```
|
|
597
446
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
## Use Cases
|
|
601
|
-
|
|
602
|
-
### ✅ Perfect For
|
|
603
|
-
|
|
604
|
-
- **Apps with async operations** (API calls, data fetching - use batched mode)
|
|
605
|
-
- **Apps that might need collaboration someday** (start simple, scale without refactoring)
|
|
606
|
-
- **Real-time collaboration** (like Figma, Notion, Google Docs)
|
|
607
|
-
- **Chat and messaging apps**
|
|
608
|
-
- **Live dashboards and monitoring**
|
|
609
|
-
- **Interactive data visualizations**
|
|
610
|
-
- **Apps with undo/redo**
|
|
611
|
-
- **Collection-based UIs** (lists, feeds, boards)
|
|
612
|
-
- **...and games!**
|
|
613
|
-
|
|
614
|
-
### 🤔 Maybe Overkill For
|
|
615
|
-
|
|
616
|
-
- Simple forms with local state only
|
|
617
|
-
- Static marketing pages
|
|
618
|
-
- Apps that will **definitely never** need real-time features
|
|
619
|
-
|
|
620
|
-
**But here's the thing:** Most successful apps eventually need collaboration, undo/redo, or live updates. With Inglorious Store, you're ready when that happens.
|
|
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.
|
|
621
448
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
## Comparison
|
|
449
|
+
### 🔗 Behavior Composition
|
|
625
450
|
|
|
626
|
-
|
|
627
|
-
| --------------------------- | ----------------- | ---------------- | ---------------- | ------------- | ------------- | --------------- | --------------- |
|
|
628
|
-
| **Integrated Immutability** | ✅ Mutative | ❌ Manual | ✅ Immer | ❌ Manual | ✅ Optional | ✅ Built-in | ✅ Observables |
|
|
629
|
-
| **Event Queue/Batching** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ Automatic |
|
|
630
|
-
| **Dispatch from Handlers** | ✅ Safe (queued) | ❌ Not allowed | ❌ Not allowed | ✅ | ✅ | ✅ | ✅ |
|
|
631
|
-
| **Redux DevTools** | ⚠️ Limited | ✅ Native | ✅ Native | ✅ Middleware | ⚠️ Limited | ✅ Vue DevTools | ⚠️ Limited |
|
|
632
|
-
| **react-redux Compatible** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ | ❌ | ❌ Vue only | ❌ |
|
|
633
|
-
| **Time-Travel Debug** | ✅ Built-in | ✅ Via DevTools | ✅ Via DevTools | ⚠️ Manual | ❌ | ⚠️ Limited | ❌ |
|
|
634
|
-
| **Entity-Based State** | ✅ First-class | ⚠️ Manual | ✅ EntityAdapter | ❌ | ❌ | ❌ | ❌ |
|
|
635
|
-
| **Pub/Sub Events** | ✅ Core pattern | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
636
|
-
| **Multiplayer-Ready** | ✅ Deterministic | ⚠️ With work | ⚠️ With work | ⚠️ With work | ❌ | ❌ | ❌ |
|
|
637
|
-
| **Testability** | ✅ Pure functions | ✅ Pure reducers | ✅ Pure reducers | ⚠️ With mocks | ⚠️ With mocks | ⚠️ With mocks | ❌ Side effects |
|
|
638
|
-
| **Learning Curve** | Medium | High | Medium | Low | Medium | Low | Medium |
|
|
639
|
-
| **Bundle Size** | Small | Small | Medium | Tiny | Small | Medium | Medium |
|
|
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):
|
|
640
452
|
|
|
641
|
-
|
|
453
|
+
```javascript
|
|
454
|
+
// Base behavior
|
|
455
|
+
const handlers = {
|
|
456
|
+
submit(entity, value) {
|
|
457
|
+
entity.value = ""
|
|
458
|
+
},
|
|
459
|
+
}
|
|
642
460
|
|
|
643
|
-
|
|
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
|
+
})
|
|
644
468
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
})
|
|
650
477
|
|
|
651
|
-
|
|
478
|
+
// Compose them together
|
|
479
|
+
const types = {
|
|
480
|
+
form: [handlers, validated, withLoading],
|
|
481
|
+
}
|
|
482
|
+
```
|
|
652
483
|
|
|
653
|
-
|
|
654
|
-
- Built-in time-travel debugging
|
|
655
|
-
- Entity/type architecture for collections
|
|
656
|
-
- Event queue prevents cascading updates
|
|
657
|
-
- 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.
|
|
658
485
|
|
|
659
|
-
|
|
486
|
+
### ⏱️ Batched Mode
|
|
660
487
|
|
|
661
|
-
|
|
662
|
-
- Better for entity collections
|
|
663
|
-
- Built-in normalization
|
|
664
|
-
- Explicit event flow
|
|
488
|
+
Process multiple events together before re-rendering:
|
|
665
489
|
|
|
666
|
-
|
|
490
|
+
```javascript
|
|
491
|
+
const store = createStore({ types, entities, mode: "batched" })
|
|
667
492
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
493
|
+
store.notify("playerMoved", { x: 100, y: 50 })
|
|
494
|
+
store.notify("enemyAttacked", { damage: 10 })
|
|
495
|
+
store.notify("particleCreated", { type: "explosion" })
|
|
671
496
|
|
|
672
|
-
|
|
497
|
+
requestAnimationFrame(() => store.update())
|
|
498
|
+
```
|
|
673
499
|
|
|
674
|
-
-
|
|
675
|
-
- Serializable state (easier persistence/sync)
|
|
676
|
-
- Deterministic (better for debugging)
|
|
677
|
-
- 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.
|
|
678
501
|
|
|
679
502
|
---
|
|
680
503
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
- Building real-time/collaborative features
|
|
684
|
-
- Managing collections of similar items
|
|
685
|
-
- Need deterministic state for multiplayer
|
|
686
|
-
- Want built-in time-travel debugging
|
|
687
|
-
- Coming from Redux and want better DX
|
|
504
|
+
## Comparison with Other State Libraries
|
|
688
505
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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 |
|
|
695
516
|
|
|
696
517
|
---
|
|
697
518
|
|
|
698
|
-
##
|
|
699
|
-
|
|
700
|
-
Adding multiplayer to an existing app is usually a massive refactor. With Inglorious Store, it's an afternoon project.
|
|
701
|
-
|
|
702
|
-
### Step 1: Your app already works locally
|
|
703
|
-
|
|
704
|
-
```javascript
|
|
705
|
-
store.notify("movePlayer", { x: 10, y: 20 })
|
|
706
|
-
store.update()
|
|
707
|
-
```
|
|
519
|
+
## API Reference
|
|
708
520
|
|
|
709
|
-
###
|
|
521
|
+
### `createStore(options)`
|
|
710
522
|
|
|
711
523
|
```javascript
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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'
|
|
715
529
|
})
|
|
716
|
-
|
|
717
|
-
// Send your events to other clients
|
|
718
|
-
const processedEvents = store.update()
|
|
719
|
-
processedEvents.forEach((event) => {
|
|
720
|
-
socket.emit("event", event)
|
|
721
|
-
})
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
**That's it.** Because your event handlers are pure functions and the state is deterministic, all clients stay perfectly in sync.
|
|
725
|
-
|
|
726
|
-
### Why This Works
|
|
727
|
-
|
|
728
|
-
1. **Deterministic:** Same events + same state = same result (always)
|
|
729
|
-
2. **Serializable:** Events are plain objects (easy to send over network)
|
|
730
|
-
3. **Ordered:** Event queue ensures predictable processing
|
|
731
|
-
4. **Conflict-free:** Last write wins, or implement custom merge logic
|
|
732
|
-
|
|
733
|
-
### Example: Collaborative Todo List
|
|
734
|
-
|
|
735
|
-
```javascript
|
|
736
|
-
// Client A adds a todo
|
|
737
|
-
store.notify("addTodo", { id: "todo1", text: "Buy milk" })
|
|
738
|
-
|
|
739
|
-
// Event gets broadcast to all clients
|
|
740
|
-
// All clients process the same event
|
|
741
|
-
// All clients end up with identical state
|
|
742
|
-
|
|
743
|
-
// Even works offline! Events queue up, sync when reconnected
|
|
744
530
|
```
|
|
745
531
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
---
|
|
749
|
-
|
|
750
|
-
## Advanced: Time-Based Updates
|
|
532
|
+
**Returns:** Redux-compatible store
|
|
751
533
|
|
|
752
|
-
|
|
534
|
+
### Types Definition
|
|
753
535
|
|
|
754
536
|
```javascript
|
|
755
537
|
const types = {
|
|
756
|
-
|
|
538
|
+
entityType: [
|
|
539
|
+
// Behavior objects
|
|
757
540
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
particle.y += particle.velocityY * dt
|
|
762
|
-
particle.life -= dt
|
|
541
|
+
eventName(entity, payload, api) {
|
|
542
|
+
entity.value = payload
|
|
543
|
+
api.notify("otherEvent", data)
|
|
763
544
|
},
|
|
764
545
|
},
|
|
546
|
+
// Behavior functions (decorators)
|
|
547
|
+
(behavior) => ({
|
|
548
|
+
eventName(entity, payload, api) {
|
|
549
|
+
// Wrap the behavior
|
|
550
|
+
behavior.eventName?.(entity, payload, api)
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
765
553
|
],
|
|
766
554
|
}
|
|
767
|
-
|
|
768
|
-
// Run at 30 FPS (good for most UIs)
|
|
769
|
-
setInterval(() => store.update(), 1000 / 30)
|
|
770
|
-
|
|
771
|
-
// Or 60 FPS (for smooth animations/games)
|
|
772
|
-
function loop() {
|
|
773
|
-
store.update()
|
|
774
|
-
requestAnimationFrame(loop)
|
|
775
|
-
}
|
|
776
|
-
loop()
|
|
777
555
|
```
|
|
778
556
|
|
|
779
|
-
|
|
557
|
+
### Event Handler API
|
|
780
558
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
---
|
|
559
|
+
Each handler receives three arguments:
|
|
784
560
|
|
|
785
|
-
|
|
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)
|
|
786
568
|
|
|
787
|
-
###
|
|
569
|
+
### Built-in Lifecycle Events
|
|
788
570
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
792
574
|
|
|
793
|
-
|
|
575
|
+
### Notify vs Dispatch
|
|
794
576
|
|
|
795
|
-
|
|
577
|
+
Both work (dispatch for Redux compatibility), but `notify` is cleaner:
|
|
796
578
|
|
|
797
579
|
```javascript
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
store.setState(snapshot) // Undo!
|
|
580
|
+
store.notify("eventName", payload)
|
|
581
|
+
store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
|
|
801
582
|
```
|
|
802
583
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
### Year 1: Competitor launches with real-time collaboration
|
|
584
|
+
---
|
|
806
585
|
|
|
807
|
-
|
|
808
|
-
socket.on("remote-event", (e) => store.notify(e.type, e.payload))
|
|
809
|
-
```
|
|
586
|
+
## Use Cases
|
|
810
587
|
|
|
811
|
-
|
|
588
|
+
### Perfect For
|
|
812
589
|
|
|
813
|
-
|
|
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
|
|
814
595
|
|
|
815
|
-
|
|
596
|
+
### Still Great For
|
|
816
597
|
|
|
817
|
-
|
|
598
|
+
- Any Redux use case (true drop-in replacement)
|
|
599
|
+
- Migration path from Redux (keep using react-redux)
|
|
818
600
|
|
|
819
601
|
---
|
|
820
602
|
|
|
821
|
-
##
|
|
603
|
+
## Part of the Inglorious Engine
|
|
822
604
|
|
|
823
|
-
|
|
824
|
-
- 🎮 **[@inglorious/engine](https://github.com/IngloriousCoderz/inglorious-engine)** - Full game engine built on this store
|
|
825
|
-
- 🌐 **[@inglorious/server](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/server)** - Server-side multiplayer support
|
|
826
|
-
- 💬 **[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.
|
|
827
606
|
|
|
828
607
|
---
|
|
829
608
|
|
|
830
609
|
## License
|
|
831
610
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
611
|
+
MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
835
612
|
|
|
836
|
-
|
|
613
|
+
Free to use, modify, and distribute.
|
|
837
614
|
|
|
838
615
|
---
|
|
839
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,22 +1,8 @@
|
|
|
1
|
-
import { createSelector as _createSelector } from "./select.js"
|
|
2
|
-
|
|
3
1
|
export function createApi(store, extras) {
|
|
4
|
-
const createSelector = (inputSelectors, resultFunc) => {
|
|
5
|
-
const selector = _createSelector(inputSelectors, resultFunc)
|
|
6
|
-
return () => selector(store.getState())
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const getTypes = () => store.getTypes()
|
|
10
|
-
|
|
11
|
-
const getEntities = () => store.getState()
|
|
12
|
-
|
|
13
|
-
const getEntity = (id) => getEntities()[id]
|
|
14
|
-
|
|
15
2
|
return {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
getEntity,
|
|
3
|
+
getTypes: store.getTypes,
|
|
4
|
+
getEntities: store.getState,
|
|
5
|
+
getEntity: (id) => store.getState()[id],
|
|
20
6
|
dispatch: store.dispatch,
|
|
21
7
|
notify: store.notify,
|
|
22
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
|
/**
|
package/src/types.js
CHANGED
|
@@ -12,8 +12,9 @@ import { pipe } from "@inglorious/utils/functions/functions.js"
|
|
|
12
12
|
* @returns {Type} The fully composed and augmented type object.
|
|
13
13
|
*/
|
|
14
14
|
export function augmentType(type) {
|
|
15
|
-
const behaviors = ensureArray(type).map(
|
|
16
|
-
|
|
15
|
+
const behaviors = ensureArray(type).map(
|
|
16
|
+
(behavior) => (type) =>
|
|
17
|
+
extend(type, typeof behavior === "function" ? behavior(type) : behavior),
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
return pipe(...behaviors)({})
|