@inglorious/store 4.0.5 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +508 -97
- package/package.json +4 -4
- package/src/entities.js +1 -1
- package/src/event-map.js +3 -3
package/README.md
CHANGED
|
@@ -3,9 +3,100 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@inglorious/store)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
**State management inspired by video games.**
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Inglorious Store brings battle-tested patterns from game development to modern web applications. If your app needs real-time updates, multiplayer features, or complex interactive state, you'll benefit from the same techniques that power multiplayer games and collaborative tools like Figma.
|
|
9
|
+
|
|
10
|
+
Perfect for: real-time collaboration, live dashboards, chat apps, interactive visualizations, and any application where state synchronization matters.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why Video Game Patterns?
|
|
15
|
+
|
|
16
|
+
Video games solved distributed state synchronization decades ago. They handle:
|
|
17
|
+
|
|
18
|
+
- **60fps updates** with thousands of entities
|
|
19
|
+
- **Multiplayer** with lag compensation and state sync
|
|
20
|
+
- **Deterministic simulation** for replays and debugging
|
|
21
|
+
- **Complex interactions** between many objects
|
|
22
|
+
|
|
23
|
+
If it works for games, it'll handle your app's state with ease.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Key Features
|
|
28
|
+
|
|
29
|
+
### 🎮 **Entity-Based State**
|
|
30
|
+
|
|
31
|
+
Define behavior once, reuse it across all instances of the same type. Perfect for managing collections (todos, messages, cart items).
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// Define behavior for ALL todos
|
|
35
|
+
const todoType = {
|
|
36
|
+
toggle(todo, id) {
|
|
37
|
+
if (todo.id !== id) return
|
|
38
|
+
todo.completed = !todo.completed
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Toggle specific todos
|
|
43
|
+
store.notify("toggle", "todo-1")
|
|
44
|
+
store.notify("toggle", "todo-2")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Important:** `toggle` is not a method—it's an **event handler**. When you notify an event, it's broadcast to **all entities** that have that handler (pub/sub pattern). Use the payload to filter which entities should respond.
|
|
48
|
+
|
|
49
|
+
### 🔄 **Event Queue with Batching**
|
|
50
|
+
|
|
51
|
+
Events are queued and processed together, preventing cascading updates and enabling predictable state changes.
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
// Dispatch multiple events
|
|
55
|
+
store.notify("increment", "counter-1")
|
|
56
|
+
store.notify("increment", "counter-2")
|
|
57
|
+
store.notify("increment", "counter-3")
|
|
58
|
+
|
|
59
|
+
// Process all at once (single React re-render)
|
|
60
|
+
store.update()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### ⏱️ **Time-Travel Debugging**
|
|
64
|
+
|
|
65
|
+
Save and replay state at any point—built-in, not an afterthought.
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
const snapshot = store.getState()
|
|
69
|
+
// ... user makes changes ...
|
|
70
|
+
store.setState(snapshot) // Instant undo
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 🌐 **Multiplayer-Ready**
|
|
74
|
+
|
|
75
|
+
Synchronize state across clients by sending serializable events. Same events + same handlers = guaranteed sync.
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
socket.on("userAction", (event) => {
|
|
79
|
+
store.notify(event.type, event.payload)
|
|
80
|
+
// All clients stay in perfect sync
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### ✍️ **Ergonomic Immutability**
|
|
85
|
+
|
|
86
|
+
Write code that looks mutable, get immutable updates automatically via [Mutative](https://mutative.js.org/).
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Looks like mutation, but creates new immutable state
|
|
90
|
+
const todoType = {
|
|
91
|
+
rename(todo, text) {
|
|
92
|
+
todo.text = text // So clean!
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 🔗 **Redux-Compatible**
|
|
98
|
+
|
|
99
|
+
Works with `react-redux` and Redux DevTools. Provides both `notify()` and `dispatch()` for compatibility.
|
|
9
100
|
|
|
10
101
|
---
|
|
11
102
|
|
|
@@ -17,154 +108,474 @@ npm install @inglorious/store
|
|
|
17
108
|
|
|
18
109
|
---
|
|
19
110
|
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
### Simple Counter Example
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
import { createStore } from "@inglorious/store"
|
|
117
|
+
|
|
118
|
+
// Types can be a single behavior (not an array) for simplicity
|
|
119
|
+
const types = {
|
|
120
|
+
counter: {
|
|
121
|
+
increment(counter) {
|
|
122
|
+
counter.value++
|
|
123
|
+
},
|
|
124
|
+
decrement(counter) {
|
|
125
|
+
counter.value--
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const entities = {
|
|
131
|
+
"counter-1": { type: "counter", value: 0 },
|
|
132
|
+
"counter-2": { type: "counter", value: 10 },
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const store = createStore({ types, entities })
|
|
136
|
+
|
|
137
|
+
// One event updates ALL counters
|
|
138
|
+
store.notify("increment")
|
|
139
|
+
store.update()
|
|
140
|
+
|
|
141
|
+
console.log(store.getState().entities["counter-1"].value) // => 1
|
|
142
|
+
console.log(store.getState().entities["counter-2"].value) // => 11
|
|
143
|
+
|
|
144
|
+
// To update just one counter, add filtering logic in the handler
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Complete Todo App Example
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
import { createStore } from "@inglorious/store"
|
|
151
|
+
import { createSelector } from "@inglorious/store/select"
|
|
152
|
+
|
|
153
|
+
// 1. Define types (can be a single behavior or array of behaviors)
|
|
154
|
+
const types = {
|
|
155
|
+
form: {
|
|
156
|
+
inputChange(entity, value) {
|
|
157
|
+
entity.value = value
|
|
158
|
+
},
|
|
159
|
+
formSubmit(entity) {
|
|
160
|
+
entity.value = ""
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
list: {
|
|
165
|
+
formSubmit(entity, value) {
|
|
166
|
+
entity.tasks.push({
|
|
167
|
+
id: entity.tasks.length + 1,
|
|
168
|
+
text: value,
|
|
169
|
+
completed: false,
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
toggleClick(entity, id) {
|
|
173
|
+
const task = entity.tasks.find((task) => task.id === id)
|
|
174
|
+
task.completed = !task.completed
|
|
175
|
+
},
|
|
176
|
+
deleteClick(entity, id) {
|
|
177
|
+
const index = entity.tasks.findIndex((task) => task.id === id)
|
|
178
|
+
entity.tasks.splice(index, 1)
|
|
179
|
+
},
|
|
180
|
+
clearClick(entity) {
|
|
181
|
+
entity.tasks = entity.tasks.filter((task) => !task.completed)
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
footer: {
|
|
186
|
+
filterClick(entity, filter) {
|
|
187
|
+
entity.activeFilter = filter
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 2. Define initial entities
|
|
193
|
+
const entities = {
|
|
194
|
+
form: {
|
|
195
|
+
type: "form",
|
|
196
|
+
value: "",
|
|
197
|
+
},
|
|
198
|
+
list: {
|
|
199
|
+
type: "list",
|
|
200
|
+
tasks: [],
|
|
201
|
+
},
|
|
202
|
+
footer: {
|
|
203
|
+
type: "footer",
|
|
204
|
+
activeFilter: "all",
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 3. Create store
|
|
209
|
+
const store = createStore({ types, entities })
|
|
210
|
+
|
|
211
|
+
// 4. Create selectors
|
|
212
|
+
const selectTasks = ({ entities }) => entities.list.tasks
|
|
213
|
+
const selectActiveFilter = ({ entities }) => entities.footer.activeFilter
|
|
214
|
+
|
|
215
|
+
const selectFilteredTasks = createSelector(
|
|
216
|
+
[selectTasks, selectActiveFilter],
|
|
217
|
+
(tasks, activeFilter) => {
|
|
218
|
+
switch (activeFilter) {
|
|
219
|
+
case "active":
|
|
220
|
+
return tasks.filter((t) => !t.completed)
|
|
221
|
+
case "completed":
|
|
222
|
+
return tasks.filter((t) => t.completed)
|
|
223
|
+
default:
|
|
224
|
+
return tasks
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// 5. Subscribe to changes
|
|
230
|
+
store.subscribe(() => {
|
|
231
|
+
console.log("Filtered tasks:", selectFilteredTasks(store.getState()))
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// 6. Dispatch events (use notify or dispatch - both work!)
|
|
235
|
+
store.notify("inputChange", "Buy milk")
|
|
236
|
+
store.notify("formSubmit", store.getState().entities.form.value)
|
|
237
|
+
store.notify("toggleClick", 1) // Only todo with id=1 will respond
|
|
238
|
+
store.notify("filterClick", "active")
|
|
239
|
+
|
|
240
|
+
// 7. Process event queue
|
|
241
|
+
store.update()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
20
246
|
## Core Concepts
|
|
21
247
|
|
|
22
|
-
|
|
248
|
+
### Pub/Sub Event Architecture
|
|
249
|
+
|
|
250
|
+
**This is not OOP with methods—it's a pub/sub (publish/subscribe) event system.**
|
|
23
251
|
|
|
24
|
-
|
|
252
|
+
When you call `store.notify('toggle', 'todo-1')`, the `toggle` event is broadcast to **all entities**. Any entity that has a `toggle` handler will process the event and decide whether to respond based on the payload.
|
|
25
253
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
254
|
+
```javascript
|
|
255
|
+
const todoType = {
|
|
256
|
+
// This handler runs for EVERY todo when 'toggle' is notified
|
|
257
|
+
toggle(todo, id) {
|
|
258
|
+
if (todo.id !== id) return // Filter: only this todo responds
|
|
259
|
+
todo.completed = !todo.completed
|
|
260
|
+
},
|
|
261
|
+
}
|
|
29
262
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
263
|
+
// This broadcasts 'toggle' to all entities
|
|
264
|
+
store.notify("toggle", "todo-1") // Only todo-1 actually updates
|
|
265
|
+
```
|
|
33
266
|
|
|
34
|
-
|
|
267
|
+
**Why this matters:**
|
|
268
|
+
|
|
269
|
+
- ✅ Multiple entities of different types can respond to the same event
|
|
270
|
+
- ✅ Enables reactive, decoupled behavior
|
|
271
|
+
- ✅ Perfect for coordinating related entities
|
|
272
|
+
- ✅ Natural fit for multiplayer/real-time sync
|
|
273
|
+
|
|
274
|
+
**Example of multiple entities responding:**
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const types = {
|
|
278
|
+
player: {
|
|
279
|
+
gameOver(player) {
|
|
280
|
+
player.active = false
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
enemy: {
|
|
284
|
+
gameOver(enemy) {
|
|
285
|
+
enemy.active = false
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
ui: {
|
|
289
|
+
gameOver(ui) {
|
|
290
|
+
ui.showGameOverScreen = true
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// One event, all three entity types respond (if they have the handler)
|
|
296
|
+
store.notify("gameOver")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Entities and Types
|
|
300
|
+
|
|
301
|
+
Your state is a collection of **entities** (instances) organized by **type** (like classes or models).
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
const entities = {
|
|
305
|
+
"item-1": { type: "cartItem", name: "Shoes", quantity: 1, price: 99 },
|
|
306
|
+
"item-2": { type: "cartItem", name: "Shirt", quantity: 2, price: 29 },
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Behaviors
|
|
311
|
+
|
|
312
|
+
Define how entities respond to events. Behaviors can be a single object or an array of composable objects.
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
// Single behavior (simple)
|
|
316
|
+
const counterType = {
|
|
317
|
+
increment(counter) {
|
|
318
|
+
counter.value++
|
|
319
|
+
},
|
|
320
|
+
decrement(counter) {
|
|
321
|
+
counter.value--
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Array of behaviors (composable)
|
|
326
|
+
const cartItemType = [
|
|
327
|
+
{
|
|
328
|
+
incrementQuantity(item) {
|
|
329
|
+
item.quantity++
|
|
330
|
+
},
|
|
331
|
+
decrementQuantity(item) {
|
|
332
|
+
if (item.quantity > 1) item.quantity--
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
applyDiscount(item, percent) {
|
|
337
|
+
item.price = item.price * (1 - percent / 100)
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Events
|
|
344
|
+
|
|
345
|
+
Events are broadcast to all relevant handlers in a pub/sub pattern.
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
// Simplest form - just the entity ID
|
|
349
|
+
store.notify("increment", "counter-1")
|
|
350
|
+
|
|
351
|
+
// With additional data
|
|
352
|
+
store.notify("applyDiscount", { id: "item-1", percent: 10 })
|
|
353
|
+
|
|
354
|
+
// Also supports dispatch() for Redux compatibility
|
|
355
|
+
store.dispatch({ type: "increment", payload: "counter-1" })
|
|
356
|
+
|
|
357
|
+
// Process the queue - this is when handlers actually run
|
|
358
|
+
store.update()
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Key insight:** Events go into a queue and are processed together during `update()`. This enables batching and prevents cascading updates within a single frame.
|
|
362
|
+
|
|
363
|
+
### Systems (Optional)
|
|
364
|
+
|
|
365
|
+
For global state logic that doesn't belong to a specific entity type.
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const systems = [
|
|
369
|
+
{
|
|
370
|
+
calculateTotal(state) {
|
|
371
|
+
state.cartTotal = Object.values(state.entities)
|
|
372
|
+
.filter((e) => e.type === "cartItem")
|
|
373
|
+
.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
]
|
|
377
|
+
```
|
|
35
378
|
|
|
36
379
|
---
|
|
37
380
|
|
|
38
|
-
## API
|
|
381
|
+
## API Reference
|
|
39
382
|
|
|
40
383
|
### `createStore(options)`
|
|
41
384
|
|
|
42
385
|
Creates a new store instance.
|
|
43
386
|
|
|
44
|
-
**
|
|
387
|
+
**Options:**
|
|
45
388
|
|
|
46
|
-
- `
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- `systems` (array, optional): An array of system objects, which define behaviors for the whole state.
|
|
389
|
+
- `types` (object): Map of type names to behaviors (single object or array)
|
|
390
|
+
- `entities` (object): Initial entities by ID
|
|
391
|
+
- `systems` (array, optional): Global event handlers
|
|
50
392
|
|
|
51
393
|
**Returns:**
|
|
52
394
|
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- `getTypes()`: Returns the augmented types configuration. Augmenting here means that the array of behaviors is merged into one single behavior.
|
|
61
|
-
- `getOriginalTypes()`: Returns the original, un-augmented behavior arrays.
|
|
62
|
-
- `reset()`: Resets the state to its initial configuration.
|
|
395
|
+
- `subscribe(listener)`: Subscribe to state changes
|
|
396
|
+
- `update(dt)`: Process event queue (optional `dt` for time-based logic)
|
|
397
|
+
- `notify(type, payload)`: Queue an event
|
|
398
|
+
- `dispatch(event)`: Redux-compatible event dispatch
|
|
399
|
+
- `getState()`: Get current immutable state
|
|
400
|
+
- `setState(newState)`: Replace entire state
|
|
401
|
+
- `reset()`: Reset to initial state
|
|
63
402
|
|
|
64
|
-
|
|
403
|
+
### `createApi(store)`
|
|
404
|
+
|
|
405
|
+
Creates a convenience wrapper with utility methods.
|
|
406
|
+
|
|
407
|
+
**Returns:**
|
|
408
|
+
|
|
409
|
+
- `createSelector(inputSelectors, resultFunc)`: Memoized selectors
|
|
410
|
+
- `getTypes()`, `getEntities()`, `getEntity(id)`: State accessors
|
|
411
|
+
- `notify(type, payload)`: Dispatch events
|
|
65
412
|
|
|
66
413
|
### `createSelector(inputSelectors, resultFunc)`
|
|
67
414
|
|
|
68
|
-
|
|
415
|
+
Create memoized, performant selectors.
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
const selectCompletedTodos = createSelector(
|
|
419
|
+
[(state) => state.entities],
|
|
420
|
+
(entities) => Object.values(entities).filter((e) => e.completed),
|
|
421
|
+
)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
69
425
|
|
|
70
|
-
|
|
426
|
+
## Use Cases
|
|
71
427
|
|
|
72
|
-
|
|
73
|
-
- `resultFunc` (function): A function that takes the results of the `inputSelectors` and returns the final computed value.
|
|
428
|
+
### ✅ Perfect For
|
|
74
429
|
|
|
75
|
-
**
|
|
430
|
+
- **Real-time collaboration** (like Figma, Google Docs)
|
|
431
|
+
- **Chat and messaging apps**
|
|
432
|
+
- **Live dashboards and monitoring**
|
|
433
|
+
- **Interactive data visualizations**
|
|
434
|
+
- **Apps with undo/redo**
|
|
435
|
+
- **Multiplayer features**
|
|
436
|
+
- **Collection-based UIs** (lists, feeds, boards)
|
|
437
|
+
- **...and games!**
|
|
76
438
|
|
|
77
|
-
|
|
439
|
+
### 🤔 Maybe Overkill For
|
|
440
|
+
|
|
441
|
+
- Simple forms with local state
|
|
442
|
+
- Static marketing pages
|
|
443
|
+
- Basic CRUD with no real-time needs
|
|
78
444
|
|
|
79
445
|
---
|
|
80
446
|
|
|
81
|
-
|
|
447
|
+
## Comparison
|
|
82
448
|
|
|
83
|
-
|
|
449
|
+
| Feature | Inglorious Store | Redux | Redux Toolkit | Zustand | Jotai | Pinia | MobX |
|
|
450
|
+
| --------------------------- | ----------------- | ------------------- | ---------------- | ------------- | ------------- | --------------- | --------------- |
|
|
451
|
+
| **Integrated Immutability** | ✅ Mutative | ❌ Manual | ✅ Immer | ❌ Manual | ✅ Optional | ✅ Built-in | ✅ Observables |
|
|
452
|
+
| **Event Queue/Batching** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ Automatic |
|
|
453
|
+
| **Dispatch from Handlers** | ✅ Safe (queued) | ❌ Not allowed | ❌ Not allowed | ✅ | ✅ | ✅ | ✅ |
|
|
454
|
+
| **Redux DevTools** | ⚠️ Limited | ✅ Native | ✅ Native | ✅ Middleware | ⚠️ Limited | ✅ Vue DevTools | ⚠️ Limited |
|
|
455
|
+
| **react-redux Compatible** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ | ❌ | ❌ Vue only | ❌ |
|
|
456
|
+
| **Time-Travel Debug** | ✅ Built-in | ✅ Via DevTools | ✅ Via DevTools | ⚠️ Manual | ❌ | ⚠️ Limited | ❌ |
|
|
457
|
+
| **Entity-Based State** | ✅ First-class | ⚠️ Manual normalize | ✅ EntityAdapter | ❌ | ❌ | ❌ | ❌ |
|
|
458
|
+
| **Pub/Sub Events** | ✅ Core pattern | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
459
|
+
| **Multiplayer-Ready** | ✅ Deterministic | ⚠️ With work | ⚠️ With work | ⚠️ With work | ❌ | ❌ | ❌ |
|
|
460
|
+
| **Testability** | ✅ Pure functions | ✅ Pure reducers | ✅ Pure reducers | ⚠️ With mocks | ⚠️ With mocks | ⚠️ With mocks | ❌ Side effects |
|
|
461
|
+
| **Learning Curve** | Medium | High | Medium | Low | Medium | Low | Medium |
|
|
462
|
+
| **Bundle Size** | Small | Small | Medium | Tiny | Small | Medium | Medium |
|
|
84
463
|
|
|
85
|
-
|
|
464
|
+
### Key Differences
|
|
86
465
|
|
|
87
|
-
|
|
466
|
+
**vs Redux/RTK:**
|
|
88
467
|
|
|
89
|
-
|
|
468
|
+
- Integrated immutability (no manual spreads)
|
|
469
|
+
- Event queue with automatic batching
|
|
470
|
+
- Can dispatch from handlers safely
|
|
471
|
+
- Entity-based architecture built-in
|
|
472
|
+
- Reusable handlers across instances
|
|
90
473
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
474
|
+
**vs Zustand:**
|
|
475
|
+
|
|
476
|
+
- Deterministic event processing (better for multiplayer)
|
|
477
|
+
- Built-in time-travel debugging
|
|
478
|
+
- Entity/type architecture for collections
|
|
479
|
+
- Event queue prevents cascading updates
|
|
480
|
+
- Redux DevTools compatible
|
|
481
|
+
|
|
482
|
+
**vs Jotai:**
|
|
483
|
+
|
|
484
|
+
- Different paradigm (events vs atoms)
|
|
485
|
+
- Better for entity collections
|
|
486
|
+
- Built-in normalization
|
|
487
|
+
- Explicit event flow
|
|
488
|
+
|
|
489
|
+
**vs Pinia:**
|
|
490
|
+
|
|
491
|
+
- React-compatible (Pinia is Vue-only)
|
|
492
|
+
- Event queue system
|
|
493
|
+
- Deterministic updates for multiplayer
|
|
494
|
+
|
|
495
|
+
**vs MobX:**
|
|
496
|
+
|
|
497
|
+
- Explicit events (less magic)
|
|
498
|
+
- Serializable state (easier persistence/sync)
|
|
499
|
+
- Deterministic (better for debugging)
|
|
500
|
+
- Redux DevTools compatible
|
|
95
501
|
|
|
96
502
|
---
|
|
97
503
|
|
|
98
|
-
|
|
504
|
+
**When to choose Inglorious Store:**
|
|
505
|
+
|
|
506
|
+
- Building real-time/collaborative features
|
|
507
|
+
- Managing collections of similar items
|
|
508
|
+
- Need deterministic state for multiplayer
|
|
509
|
+
- Want built-in time-travel debugging
|
|
510
|
+
- Coming from Redux and want better DX
|
|
511
|
+
|
|
512
|
+
**When to choose alternatives:**
|
|
513
|
+
|
|
514
|
+
- **Zustand/Jotai**: Simple apps, prefer minimal API
|
|
515
|
+
- **Redux Toolkit**: Large team, established Redux patterns
|
|
516
|
+
- **Pinia**: Vue ecosystem
|
|
517
|
+
- **MobX**: Prefer reactive/observable patterns
|
|
518
|
+
|
|
519
|
+
---
|
|
99
520
|
|
|
100
|
-
|
|
521
|
+
## Advanced: Real-Time Sync
|
|
101
522
|
|
|
102
523
|
```javascript
|
|
103
|
-
|
|
104
|
-
|
|
524
|
+
// Client-side
|
|
525
|
+
socket.on("server-event", (event) => {
|
|
526
|
+
store.notify(event.type, event.payload)
|
|
527
|
+
store.update()
|
|
528
|
+
})
|
|
105
529
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
entity.position[2] += payload.dz || 0
|
|
113
|
-
},
|
|
114
|
-
}
|
|
530
|
+
// Send local events to server
|
|
531
|
+
store.subscribe(() => {
|
|
532
|
+
const state = store.getState()
|
|
533
|
+
socket.emit("state-update", serializeState(state))
|
|
534
|
+
})
|
|
535
|
+
```
|
|
115
536
|
|
|
116
|
-
|
|
117
|
-
update: (entity, dt) => {
|
|
118
|
-
// You can use utility functions for easy vector operations
|
|
119
|
-
entity.position = add(entity.position, scale(entity.velocity, dt))
|
|
120
|
-
},
|
|
121
|
-
}
|
|
537
|
+
---
|
|
122
538
|
|
|
123
|
-
|
|
539
|
+
## Advanced: Time-Based Updates
|
|
540
|
+
|
|
541
|
+
For animations or continuous updates (like in games):
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
124
544
|
const types = {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
545
|
+
particle: [
|
|
546
|
+
{
|
|
547
|
+
update(particle, dt) {
|
|
548
|
+
// dt = delta time in milliseconds
|
|
549
|
+
particle.x += particle.velocityX * dt
|
|
550
|
+
particle.y += particle.velocityY * dt
|
|
551
|
+
particle.life -= dt
|
|
552
|
+
},
|
|
553
|
+
},
|
|
129
554
|
],
|
|
130
555
|
}
|
|
131
556
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
velocity: v(0.0625, 0, 0),
|
|
138
|
-
},
|
|
557
|
+
// In your game/animation loop
|
|
558
|
+
function loop(timestamp) {
|
|
559
|
+
const dt = timestamp - lastTime
|
|
560
|
+
store.update(dt)
|
|
561
|
+
requestAnimationFrame(loop)
|
|
139
562
|
}
|
|
563
|
+
```
|
|
140
564
|
|
|
141
|
-
|
|
142
|
-
const store = createStore({ types, entities })
|
|
143
|
-
const api = createApi(store)
|
|
565
|
+
---
|
|
144
566
|
|
|
145
|
-
|
|
146
|
-
const selectPlayerPosition = api.createSelector(
|
|
147
|
-
[(state) => state.entities.player1],
|
|
148
|
-
(player) => player.position,
|
|
149
|
-
)
|
|
567
|
+
## Part of the Inglorious Engine
|
|
150
568
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
569
|
+
This store powers the [Inglorious Engine](https://github.com/IngloriousCoderz/inglorious-engine), a functional game engine. But you don't need to build games to benefit from game development patterns!
|
|
570
|
+
|
|
571
|
+
---
|
|
155
572
|
|
|
156
|
-
|
|
157
|
-
console.log("Initial player position:", selectPlayerPosition()) // => v(0, 0, 0)
|
|
573
|
+
## License
|
|
158
574
|
|
|
159
|
-
|
|
160
|
-
api.notify("move", { id: "player1", dx: 5, dz: 5 })
|
|
575
|
+
MIT
|
|
161
576
|
|
|
162
|
-
|
|
163
|
-
console.log("Position after notify:", selectPlayerPosition()) // => v(0, 0, 0)
|
|
577
|
+
---
|
|
164
578
|
|
|
165
|
-
|
|
166
|
-
store.update(16) // Pass delta time
|
|
167
|
-
// Console output from subscriber: "State updated! v(6, 0, 5)"
|
|
579
|
+
## Contributing
|
|
168
580
|
|
|
169
|
-
|
|
170
|
-
```
|
|
581
|
+
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "A state manager inspired by Redux, but tailored for the specific needs of game development.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"mutative": "^1.3.0",
|
|
37
|
-
"@inglorious/utils": "3.
|
|
37
|
+
"@inglorious/utils": "3.6.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@inglorious/utils": "3.
|
|
40
|
+
"@inglorious/utils": "3.6.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"prettier": "^3.6.2",
|
|
44
44
|
"vite": "^7.1.3",
|
|
45
45
|
"vitest": "^1.6.1",
|
|
46
|
-
"@inglorious/eslint-config": "1.0.
|
|
46
|
+
"@inglorious/eslint-config": "1.0.1"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">= 22"
|
package/src/entities.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { map } from "@inglorious/utils/data-structures/object.js"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {Object.<string, any>} Entity - An object representing
|
|
4
|
+
* @typedef {Object.<string, any>} Entity - An object representing an entity.
|
|
5
5
|
* @typedef {Object.<string, Entity>} Entities - A collection of named entities.
|
|
6
6
|
*/
|
|
7
7
|
|
package/src/event-map.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object.<string, any>} Type - An object representing an augmented entity type.
|
|
3
|
-
* @typedef {Object.<string, any>} Entity - An object representing a
|
|
3
|
+
* @typedef {Object.<string, any>} Entity - An object representing a entity.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A class to manage the mapping of event names to the entity IDs that handle them.
|
|
8
|
-
* This is used for optimized event handling
|
|
8
|
+
* This is used for optimized event handling.
|
|
9
9
|
*/
|
|
10
10
|
export class EventMap {
|
|
11
11
|
/**
|
|
12
12
|
* Creates an instance of EventMap and initializes it with entities and their types.
|
|
13
13
|
*
|
|
14
14
|
* @param {Object.<string, Type>} types - An object containing all augmented type definitions.
|
|
15
|
-
* @param {Object.<string, Entity>} entities - An object containing all
|
|
15
|
+
* @param {Object.<string, Entity>} entities - An object containing all entities.
|
|
16
16
|
*/
|
|
17
17
|
constructor(types, entities) {
|
|
18
18
|
/**
|