@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 CHANGED
@@ -3,203 +3,180 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/@inglorious/store.svg)](https://www.npmjs.com/package/@inglorious/store)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- **Build apps that are already multiplayer-ready.**
6
+ A Redux-compatible state management library inspired by game development architecture.
7
7
 
8
- Inglorious Store uses battle-tested patterns from game development to give you an architecture that scales from simple solo apps to real-time collaboration—without refactoring. Start with a basic todo list today. Add collaborative features next year. Same code, zero rewrites.
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 Video Game Patterns?
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
- - **Deterministic event processing** - same events + same handlers = guaranteed identical state
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
- These patterns aren't just for games. They're perfect for any app that might need:
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
- - Real-time collaboration (like Notion, Figma)
26
- - Live updates (dashboards, chat)
27
- - Undo/redo and time-travel debugging
28
- - Multiplayer features
18
+ **Key benefits:**
29
19
 
30
- **The best part?** You get this architecture from day one, even for simple apps. When you need these features later, they're already built-in.
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 apps**, also install the React bindings:
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
- ## Update Modes
51
-
52
- Inglorious Store supports two update modes:
39
+ ## Quick Comparison: Redux vs RTK vs Inglorious Store
53
40
 
54
- ### Eager Mode (default) - Like Redux
41
+ ### Redux
55
42
 
56
43
  ```javascript
57
- const store = createStore({ types, entities }) // mode: "eager" is default
58
- store.notify("addTodo", { text: "Buy milk" })
59
- // State updates immediately, no need to call update()
60
- ```
61
-
62
- **Best for:** Simple apps with synchronous logic.
63
-
64
- **Limitation:** If an event handler needs to dispatch another event, only the first event processes. Use batched mode for event chains.
65
-
66
- ### Batched Mode - Like game engines
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
- ```javascript
69
- const store = createStore({ types, entities, mode: "batched" })
70
- store.notify("addTodo", { text: "Buy milk" })
71
- store.notify("toggleTodo", "todo1")
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
- **Best for:**
63
+ ### Redux Toolkit
76
64
 
77
- - Apps with async operations (API calls, data fetching)
78
- - Event handlers that dispatch other events
79
- - Games, animations, or high-frequency updates
80
- - Explicit control over when state updates
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
- **Why batched mode for async?**
81
+ const store = configureStore({
82
+ reducer: { todos: todosSlice.reducer },
83
+ })
84
+ ```
83
85
 
84
- When fetching data from an API, you typically need two events: one to initiate the fetch, and another to store the result. Batched mode allows this pattern:
86
+ ### Inglorious Store
85
87
 
86
88
  ```javascript
89
+ // Define entity types and their behavior
87
90
  const types = {
88
91
  todoList: {
89
- async fetchTodos(entity, payload, api) {
90
- const response = await fetch("/api/todos")
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
- // In your app
105
- store.notify("fetchTodos")
106
- await store.update() // Both fetchTodos AND todosReceived process together
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
- // Toggle specific todos
129
- store.notify("toggle", "todo1")
130
- store.notify("toggle", "todo2")
104
+ // Create store
105
+ const store = createStore({ types, entities })
131
106
  ```
132
107
 
133
- > **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.
134
-
135
- ### 🔄 **Event Queue with Batching**
108
+ **Key differences:**
136
109
 
137
- Events are queued and processed together in batched mode, preventing cascading updates and enabling predictable state changes.
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
- ```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
- // Process all at once (single React re-render)
148
- store.update()
149
- ```
118
+ ## Core Concepts
150
119
 
151
- ### ⏱️ **Time-Travel Debugging**
120
+ ### 🎮 Entities and Types
152
121
 
153
- Save and replay state at any point—built-in, not an afterthought.
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 snapshot = store.getState()
157
- // ... user makes changes ...
158
- store.setState(snapshot) // Instant undo
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
- Synchronize state across clients by sending serializable events. Same events + same handlers = guaranteed sync.
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
- ```javascript
166
- // Start building solo
167
- store.notify("addTodo", { text: "Buy milk" })
149
+ **Why this matters:**
168
150
 
169
- // Add multiplayer later in ~10 lines
170
- socket.on("remote-event", (event) => {
171
- store.notify(event.type, event.payload)
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
- ### ✍️ **Ergonomic Immutability**
155
+ ### 🔄 Event Handlers (Not Reducers)
177
156
 
178
- Write code that looks mutable, get immutable updates automatically via [Mutative](https://mutative.js.org/).
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
- // Looks like mutation, but creates new immutable state
182
- const todoType = {
183
- rename(todo, text) {
184
- todo.text = text // So clean!
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
- ## Quick Start
171
+ ## Installation & Setup
196
172
 
197
- ### Simple Counter Example
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
- // Types can be a single behavior (not an array) for simplicity
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
- // One event updates ALL counters
222
- store.notify("increment")
223
- store.update()
224
-
225
- console.log(store.getState().counter1.value) // => 1
226
- console.log(store.getState().counter2.value) // => 11
199
+ // 4. Use with react-redux
200
+ function App() {
201
+ return (
202
+ <Provider store={store}>
203
+ <Counter />
204
+ </Provider>
205
+ )
206
+ }
227
207
 
228
- // To update just one counter, add filtering logic in the handler
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
- ### Complete Todo App Example
222
+ ### With @inglorious/react-store (Recommended)
232
223
 
233
224
  ```javascript
234
- import { createStore } from "@inglorious/store"
235
- import { createSelector } from "@inglorious/store/select"
225
+ import { createReactStore } from "@inglorious/react-store"
236
226
 
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
- },
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
- footer: {
270
- filterClick(entity, filter) {
271
- entity.activeFilter = filter
272
- },
273
- },
229
+ function App() {
230
+ return (
231
+ <Provider>
232
+ <Counter />
233
+ </Provider>
234
+ ) // No store prop needed
274
235
  }
275
236
 
276
- // 2. Define initial entities
277
- const entities = {
278
- form: {
279
- type: "form",
280
- value: "",
281
- },
282
- list: {
283
- type: "list",
284
- tasks: [],
285
- },
286
- footer: {
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 Concepts
253
+ ## Core Features
331
254
 
332
- ### Pub/Sub Event Architecture
255
+ ### 🎮 Entity-Based State
333
256
 
334
- **This is not OOP with methods—it's a pub/sub (publish/subscribe) event system.**
257
+ The real power: add entities dynamically without code changes.
335
258
 
336
- When you call `store.notify('toggle', 'todo1')`, 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.
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
- const todoType = {
340
- // This handler runs for EVERY todo when 'toggle' is notified
341
- toggle(todo, id) {
342
- if (todo.id !== id) return // Filter: only this todo responds
343
- todo.completed = !todo.completed
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
- **Why this matters:**
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
- player: {
363
- gameOver(player) {
364
- player.active = false
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
- // One event, all three entity types respond (if they have the handler)
380
- store.notify("gameOver")
283
+ const entities = {
284
+ work: { type: "todoList", todos: [] },
285
+ personal: { type: "todoList", todos: [] },
286
+ shopping: { type: "todoList", todos: [] },
287
+ }
381
288
  ```
382
289
 
383
- ### Entities and Types
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
- const entities = {
389
- item1: { type: "cartItem", name: "Shoes", quantity: 1, price: 99 },
390
- item2: { type: "cartItem", name: "Shirt", quantity: 2, price: 29 },
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
- ### Behaviors
300
+ **Lifecycle events:**
395
301
 
396
- Define how entities respond to events. Behaviors can be a single object or an array of composable objects.
302
+ Inglorious Store provides three built-in lifecycle events that are broadcast like any other event:
397
303
 
398
- ```javascript
399
- // Single behavior (simple)
400
- const counterType = {
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
- // Array of behaviors (composable)
410
- const cartItemType = [
411
- {
412
- incrementQuantity(item) {
413
- item.quantity++
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
- decrementQuantity(item) {
416
- if (item.quantity > 1) item.quantity--
318
+
319
+ destroy(entity, id) {
320
+ if (entity.id !== id) return
321
+ console.log(`Archived list: ${entity.id}`)
417
322
  },
418
- },
419
- {
420
- applyDiscount(item, percent) {
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
- ### Behavior Composition with Functions
331
+ ### 🔊 Event Broadcasting
428
332
 
429
- Behaviors can also be functions that wrap and extend other behaviors. This enables decorator-like patterns and middleware:
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
- // Base behaviors
433
- const baseHandlers = {
434
- formSubmit(entity, value) {
435
- entity.value = value
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
- // Function that adds validation
440
- const validated = (behavior) => ({
441
- formSubmit(entity, value, api) {
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
- **Why function composition for behaviors:**
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
- // Simplest form - just the entity ID
479
- store.notify("increment", "counter1")
480
-
481
- // With additional data
482
- store.notify("applyDiscount", { id: "item1", percent: 10 })
483
-
484
- // Also supports dispatch() for Redux compatibility
485
- store.dispatch({ type: "increment", payload: "counter1" })
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
- // Process the queue - this is when handlers actually run
488
- // (In eager mode, this happens automatically)
489
- store.update()
374
+ // One notify call, all three entity types respond
375
+ store.notify("taskCompleted", "task123")
490
376
  ```
491
377
 
492
- **Key insight:** Events go into a queue and are processed together during `update()`. This enables batching and prevents cascading updates within a single frame.
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
- Systems are global event handlers that can coordinate updates across **multiple entities at once**. Unlike entity handlers (which run once per entity), a system runs **once per event** and has write-access to the entire state.
380
+ ### Async Operations
497
381
 
498
- **When you need a system:**
382
+ This is where the choice of "where does my logic live?" matters.
499
383
 
500
- - Multiple entities need to update based on relationships between them
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
- **Example: Inventory Weight Limits**
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
- item: {
511
- addToInventory(item, newItemData) {
512
- // Individual items don't know about other items
513
- // Can't check total weight here!
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
- const systems = [
519
- {
520
- addToInventory(state, newItemData) {
521
- // Calculate total weight across ALL items
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
- // Add the new item
537
- const newId = `item${Date.now()}`
538
- state[newId] = {
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
- **Why this needs a system:**
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
- - Requires reading **all items** to calculate total weight
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
- **Another example: Multiplayer Turn System**
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
- endTurn(state, playerId) {
561
- // Find current player
562
- const players = Object.values(state).filter((e) => e.type === "player")
563
- const currentPlayer = players.find((p) => p.id === playerId)
564
-
565
- // Mark current player's turn as ended
566
- currentPlayer.isTurn = false
567
- currentPlayer.actionsRemaining = 0
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
- Create memoized, performant selectors.
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
- ### Perfect For
449
+ ### 🔗 Behavior Composition
648
450
 
649
- - **Apps with async operations** (API calls, data fetching - use batched mode)
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
- ### 🤔 Maybe Overkill For
660
-
661
- - Simple forms with local state only
662
- - Static marketing pages
663
- - Apps that will **definitely never** need real-time features
664
-
665
- **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.
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
- **vs Redux/RTK:**
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
- - Integrated immutability (no manual spreads)
691
- - Event queue with automatic batching
692
- - Can dispatch from handlers safely
693
- - Entity-based architecture built-in
694
- - Reusable handlers across instances
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
- **vs Zustand:**
478
+ // Compose them together
479
+ const types = {
480
+ form: [handlers, validated, withLoading],
481
+ }
482
+ ```
697
483
 
698
- - Deterministic event processing (better for multiplayer)
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
- **vs Jotai:**
486
+ ### ⏱️ Batched Mode
705
487
 
706
- - Different paradigm (events vs atoms)
707
- - Better for entity collections
708
- - Built-in normalization
709
- - Explicit event flow
488
+ Process multiple events together before re-rendering:
710
489
 
711
- **vs Pinia:**
490
+ ```javascript
491
+ const store = createStore({ types, entities, mode: "batched" })
712
492
 
713
- - React-compatible (Pinia is Vue-only)
714
- - Event queue system
715
- - Deterministic updates for multiplayer
493
+ store.notify("playerMoved", { x: 100, y: 50 })
494
+ store.notify("enemyAttacked", { damage: 10 })
495
+ store.notify("particleCreated", { type: "explosion" })
716
496
 
717
- **vs MobX:**
497
+ requestAnimationFrame(() => store.update())
498
+ ```
718
499
 
719
- - Explicit events (less magic)
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
- **When to choose Inglorious Store:**
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
- - **Zustand/Jotai**: Simple apps, prefer minimal API
737
- - **Redux Toolkit**: Large team, established Redux patterns
738
- - **Pinia**: Vue ecosystem
739
- - **MobX**: Prefer reactive/observable patterns
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
- ## Advanced: Real-Time Sync
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
- ### Step 2: Add WebSocket (literally ~10 lines)
521
+ ### `createStore(options)`
755
522
 
756
523
  ```javascript
757
- // Receive events from other clients
758
- socket.on("remote-event", (event) => {
759
- store.notify(event.type, event.payload)
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
- **That's it.** Because your event handlers are pure functions and the state is deterministic, all clients stay perfectly in sync.
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
- For animations, games, or any time-dependent logic, you can run a continuous update loop:
534
+ ### Types Definition
798
535
 
799
536
  ```javascript
800
537
  const types = {
801
- particle: [
538
+ entityType: [
539
+ // Behavior objects
802
540
  {
803
- update(particle, dt) {
804
- // dt = delta time in milliseconds
805
- particle.x += particle.velocityX * dt
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
- **For typical apps (todos, forms, dashboards):** Use eager mode (default). No loop needed.
557
+ ### Event Handler API
825
558
 
826
- **For real-time apps (games, animations, live data):** Use batched mode with a loop for smooth, consistent updates.
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
- ## The Path from Solo to Multiplayer
569
+ ### Built-in Lifecycle Events
831
570
 
832
- ### Week 1: Build a simple todo app
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
- ```javascript
835
- store.notify("addTodo", { text: "Buy milk" })
836
- ```
575
+ ### Notify vs Dispatch
837
576
 
838
- _Works great. Clean architecture. Nothing fancy._
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
- const snapshot = store.getState()
844
- // ... user makes changes ...
845
- store.setState(snapshot) // Undo!
580
+ store.notify("eventName", payload)
581
+ store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
846
582
  ```
847
583
 
848
- _Already built-in. No refactoring needed._
849
-
850
- ### Year 1: Competitor launches with real-time collaboration
584
+ ---
851
585
 
852
- ```javascript
853
- socket.on("remote-event", (e) => store.notify(e.type, e.payload))
854
- ```
586
+ ## Use Cases
855
587
 
856
- _Add multiplayer in an afternoon. You win._
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
- ## Part of the Inglorious Engine
596
+ ### Still Great For
861
597
 
862
- 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!
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
- ## What's Next?
603
+ ## Part of the Inglorious Engine
867
604
 
868
- - 📖 **[@inglorious/react-store](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/react-store)** - React integration with hooks
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
- **MIT License - Free and open source**
878
-
879
- Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
611
+ MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
880
612
 
881
- You're free to use, modify, and distribute this software. See [LICENSE](../../LICENSE) for details.
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": "5.5.0",
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,
@@ -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({ type, payload })
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
  /**