@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 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
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
- 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
39
+ ## Quick Comparison: Redux vs RTK vs Inglorious Store
51
40
 
52
- Inglorious Store supports two update modes:
53
-
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.
108
+ **Key differences:**
134
109
 
135
- ### 🔄 **Event Queue with Batching**
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
- Events are queued and processed together in batched mode, preventing cascading updates and enabling predictable state changes.
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
- // 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,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
- // 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"
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
- 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
  }
249
+ ```
291
250
 
292
- // 3. Create store
293
- const store = createStore({ types, entities })
251
+ ---
294
252
 
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
- )
253
+ ## Core Features
312
254
 
313
- // 5. Subscribe to changes
314
- store.subscribe(() => {
315
- console.log("Filtered tasks:", selectFilteredTasks(store.getState()))
316
- })
255
+ ### 🎮 Entity-Based State
317
256
 
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")
257
+ The real power: add entities dynamically without code changes.
323
258
 
324
- // 7. Process event queue (in eager mode this happens automatically)
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
- ## Core Concepts
272
+ **Inglorious Store:** Same behavior, no duplication:
331
273
 
332
- ### Pub/Sub Event Architecture
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
- **This is not OOP with methods—it's a pub/sub (publish/subscribe) event system.**
283
+ const entities = {
284
+ work: { type: "todoList", todos: [] },
285
+ personal: { type: "todoList", todos: [] },
286
+ shopping: { type: "todoList", todos: [] },
287
+ }
288
+ ```
335
289
 
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.
290
+ **The kicker:** Add a new list at runtime:
337
291
 
338
292
  ```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
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 broadcasts 'toggle' to all entities
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
- **Why this matters:**
300
+ **Lifecycle events:**
352
301
 
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
302
+ Inglorious Store provides three built-in lifecycle events that are broadcast like any other event:
357
303
 
358
- **Example of multiple entities responding:**
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
- player: {
363
- gameOver(player) {
364
- player.active = false
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
- enemy: {
368
- gameOver(enemy) {
369
- enemy.active = false
318
+
319
+ destroy(entity, id) {
320
+ if (entity.id !== id) return
321
+ console.log(`Archived list: ${entity.id}`)
370
322
  },
371
- },
372
- ui: {
373
- gameOver(ui) {
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
- ### Entities and Types
331
+ ### 🔊 Event Broadcasting
384
332
 
385
- Your state is a collection of **entities** (instances) organized by **type** (like classes or models).
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 entities = {
389
- item1: { type: "cartItem", name: "Shoes", quantity: 1, price: 99 },
390
- item2: { type: "cartItem", name: "Shirt", quantity: 2, price: 29 },
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
- ### Behaviors
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
- Define how entities respond to events. Behaviors can be a single object or an array of composable objects.
352
+ **Multiple types responding to the same event:**
397
353
 
398
354
  ```javascript
399
- // Single behavior (simple)
400
- const counterType = {
401
- increment(counter) {
402
- counter.value++
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
- decrementQuantity(item) {
416
- if (item.quantity > 1) item.quantity--
361
+ },
362
+ stats: {
363
+ taskCompleted(entity, taskId) {
364
+ entity.completedCount++
417
365
  },
418
366
  },
419
- {
420
- applyDiscount(item, percent) {
421
- item.price = item.price * (1 - percent / 100)
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
- // Process the queue - this is when handlers actually run
442
- // (In eager mode, this happens automatically)
443
- store.update()
374
+ // One notify call, all three entity types respond
375
+ store.notify("taskCompleted", "task123")
444
376
  ```
445
377
 
446
- **Key insight:** Events go into a queue and are processed together during `update()`. This enables batching and prevents cascading updates within a single frame.
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
- 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
451
381
 
452
- **When you need a system:**
382
+ This is where the choice of "where does my logic live?" matters.
453
383
 
454
- - Multiple entities need to update based on relationships between them
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
- **Example: Inventory Weight Limits**
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
- item: {
465
- addToInventory(item, newItemData) {
466
- // Individual items don't know about other items
467
- // 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
+ }
468
400
  },
469
- },
470
- }
471
401
 
472
- const systems = [
473
- {
474
- addToInventory(state, newItemData) {
475
- // Calculate total weight across ALL items
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
- // Add the new item
491
- const newId = `item${Date.now()}`
492
- state[newId] = {
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
- **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)
503
421
 
504
- - Requires reading **all items** to calculate total weight
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
- **Another example: Multiplayer Turn System**
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
- endTurn(state, playerId) {
515
- // Find current player
516
- const players = Object.values(state).filter((e) => e.type === "player")
517
- const currentPlayer = players.find((p) => p.id === playerId)
518
-
519
- // Mark current player's turn as ended
520
- currentPlayer.isTurn = false
521
- currentPlayer.actionsRemaining = 0
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
- ## API Reference
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
- | Feature | Inglorious Store | Redux | Redux Toolkit | Zustand | Jotai | Pinia | MobX |
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
- ### Key Differences
453
+ ```javascript
454
+ // Base behavior
455
+ const handlers = {
456
+ submit(entity, value) {
457
+ entity.value = ""
458
+ },
459
+ }
642
460
 
643
- **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
+ })
644
468
 
645
- - Integrated immutability (no manual spreads)
646
- - Event queue with automatic batching
647
- - Can dispatch from handlers safely
648
- - Entity-based architecture built-in
649
- - 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
+ })
650
477
 
651
- **vs Zustand:**
478
+ // Compose them together
479
+ const types = {
480
+ form: [handlers, validated, withLoading],
481
+ }
482
+ ```
652
483
 
653
- - Deterministic event processing (better for multiplayer)
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
- **vs Jotai:**
486
+ ### ⏱️ Batched Mode
660
487
 
661
- - Different paradigm (events vs atoms)
662
- - Better for entity collections
663
- - Built-in normalization
664
- - Explicit event flow
488
+ Process multiple events together before re-rendering:
665
489
 
666
- **vs Pinia:**
490
+ ```javascript
491
+ const store = createStore({ types, entities, mode: "batched" })
667
492
 
668
- - React-compatible (Pinia is Vue-only)
669
- - Event queue system
670
- - Deterministic updates for multiplayer
493
+ store.notify("playerMoved", { x: 100, y: 50 })
494
+ store.notify("enemyAttacked", { damage: 10 })
495
+ store.notify("particleCreated", { type: "explosion" })
671
496
 
672
- **vs MobX:**
497
+ requestAnimationFrame(() => store.update())
498
+ ```
673
499
 
674
- - Explicit events (less magic)
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
- **When to choose Inglorious Store:**
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
- **When to choose alternatives:**
690
-
691
- - **Zustand/Jotai**: Simple apps, prefer minimal API
692
- - **Redux Toolkit**: Large team, established Redux patterns
693
- - **Pinia**: Vue ecosystem
694
- - **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 |
695
516
 
696
517
  ---
697
518
 
698
- ## Advanced: Real-Time Sync
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
- ### Step 2: Add WebSocket (literally ~10 lines)
521
+ ### `createStore(options)`
710
522
 
711
523
  ```javascript
712
- // Receive events from other clients
713
- socket.on("remote-event", (event) => {
714
- store.notify(event.type, event.payload)
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
- This is exactly how multiplayer games work. Now your app can too.
747
-
748
- ---
749
-
750
- ## Advanced: Time-Based Updates
532
+ **Returns:** Redux-compatible store
751
533
 
752
- For animations, games, or any time-dependent logic, you can run a continuous update loop:
534
+ ### Types Definition
753
535
 
754
536
  ```javascript
755
537
  const types = {
756
- particle: [
538
+ entityType: [
539
+ // Behavior objects
757
540
  {
758
- update(particle, dt) {
759
- // dt = delta time in milliseconds
760
- particle.x += particle.velocityX * dt
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
- **For typical apps (todos, forms, dashboards):** Use eager mode (default). No loop needed.
557
+ ### Event Handler API
780
558
 
781
- **For real-time apps (games, animations, live data):** Use batched mode with a loop for smooth, consistent updates.
782
-
783
- ---
559
+ Each handler receives three arguments:
784
560
 
785
- ## The Path from Solo to Multiplayer
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
- ### Week 1: Build a simple todo app
569
+ ### Built-in Lifecycle Events
788
570
 
789
- ```javascript
790
- store.notify("addTodo", { text: "Buy milk" })
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
- _Works great. Clean architecture. Nothing fancy._
575
+ ### Notify vs Dispatch
794
576
 
795
- ### Month 6: Users love it, ask for undo/redo
577
+ Both work (dispatch for Redux compatibility), but `notify` is cleaner:
796
578
 
797
579
  ```javascript
798
- const snapshot = store.getState()
799
- // ... user makes changes ...
800
- store.setState(snapshot) // Undo!
580
+ store.notify("eventName", payload)
581
+ store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
801
582
  ```
802
583
 
803
- _Already built-in. No refactoring needed._
804
-
805
- ### Year 1: Competitor launches with real-time collaboration
584
+ ---
806
585
 
807
- ```javascript
808
- socket.on("remote-event", (e) => store.notify(e.type, e.payload))
809
- ```
586
+ ## Use Cases
810
587
 
811
- _Add multiplayer in an afternoon. You win._
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
- ## Part of the Inglorious Engine
596
+ ### Still Great For
816
597
 
817
- 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)
818
600
 
819
601
  ---
820
602
 
821
- ## What's Next?
603
+ ## Part of the Inglorious Engine
822
604
 
823
- - 📖 **[@inglorious/react-store](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/react-store)** - React integration with hooks
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
- **MIT License - Free and open source**
833
-
834
- Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
611
+ MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
835
612
 
836
- You're free to use, modify, and distribute this software. See [LICENSE](../../LICENSE) for details.
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": "5.4.1",
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
- createSelector,
17
- getTypes,
18
- getEntities,
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,
@@ -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
  /**
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((fn) =>
16
- typeof fn !== "function" ? (type) => extend(type, fn) : fn,
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)({})