@inglorious/store 5.2.0 โ 5.4.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 +327 -67
- package/package.json +3 -3
- package/src/store.js +7 -9
- package/src/store.test.js +5 -1
package/README.md
CHANGED
|
@@ -13,14 +13,100 @@ Why settle for state management that wasn't designed for real-time sync? Games s
|
|
|
13
13
|
|
|
14
14
|
## Why Video Game Patterns?
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Games solved the hardest real-time problems: syncing state across laggy networks with hundreds of players at 60fps. They use:
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
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
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
These patterns aren't just for games. They're perfect for any app that might need:
|
|
24
|
+
|
|
25
|
+
- Real-time collaboration (like Notion, Figma)
|
|
26
|
+
- Live updates (dashboards, chat)
|
|
27
|
+
- Undo/redo and time-travel debugging
|
|
28
|
+
- Multiplayer features
|
|
29
|
+
|
|
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.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```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
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
See [@inglorious/react-store](https://github.com/IngloriousCoderz/inglorious-engine/tree/main/packages/react-store) for React-specific documentation.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Update Modes
|
|
51
|
+
|
|
52
|
+
Inglorious Store supports two update modes:
|
|
53
|
+
|
|
54
|
+
### Eager Mode (default) - Like Redux
|
|
55
|
+
|
|
56
|
+
```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
|
|
67
|
+
|
|
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
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Best for:**
|
|
76
|
+
|
|
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
|
|
81
|
+
|
|
82
|
+
**Why batched mode for async?**
|
|
83
|
+
|
|
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:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const types = {
|
|
88
|
+
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
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
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.
|
|
24
110
|
|
|
25
111
|
---
|
|
26
112
|
|
|
@@ -40,21 +126,23 @@ const todoType = {
|
|
|
40
126
|
}
|
|
41
127
|
|
|
42
128
|
// Toggle specific todos
|
|
43
|
-
store.notify("toggle", "
|
|
44
|
-
store.notify("toggle", "
|
|
129
|
+
store.notify("toggle", "todo1")
|
|
130
|
+
store.notify("toggle", "todo2")
|
|
45
131
|
```
|
|
46
132
|
|
|
47
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.
|
|
48
134
|
|
|
49
135
|
### ๐ **Event Queue with Batching**
|
|
50
136
|
|
|
51
|
-
Events are queued and processed together, preventing cascading updates and enabling predictable state changes.
|
|
137
|
+
Events are queued and processed together in batched mode, preventing cascading updates and enabling predictable state changes.
|
|
52
138
|
|
|
53
139
|
```javascript
|
|
140
|
+
const store = createStore({ types, entities, mode: "batched" })
|
|
141
|
+
|
|
54
142
|
// Dispatch multiple events
|
|
55
|
-
store.notify("increment", "
|
|
56
|
-
store.notify("increment", "
|
|
57
|
-
store.notify("increment", "
|
|
143
|
+
store.notify("increment", "counter1")
|
|
144
|
+
store.notify("increment", "counter2")
|
|
145
|
+
store.notify("increment", "counter3")
|
|
58
146
|
|
|
59
147
|
// Process all at once (single React re-render)
|
|
60
148
|
store.update()
|
|
@@ -75,9 +163,13 @@ store.setState(snapshot) // Instant undo
|
|
|
75
163
|
Synchronize state across clients by sending serializable events. Same events + same handlers = guaranteed sync.
|
|
76
164
|
|
|
77
165
|
```javascript
|
|
78
|
-
|
|
166
|
+
// Start building solo
|
|
167
|
+
store.notify("addTodo", { text: "Buy milk" })
|
|
168
|
+
|
|
169
|
+
// Add multiplayer later in ~10 lines
|
|
170
|
+
socket.on("remote-event", (event) => {
|
|
79
171
|
store.notify(event.type, event.payload)
|
|
80
|
-
//
|
|
172
|
+
// States stay perfectly in sync across all clients
|
|
81
173
|
})
|
|
82
174
|
```
|
|
83
175
|
|
|
@@ -100,14 +192,6 @@ Works with `react-redux` and Redux DevTools. Provides both `notify()` and `dispa
|
|
|
100
192
|
|
|
101
193
|
---
|
|
102
194
|
|
|
103
|
-
## Installation
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
npm install @inglorious/store
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
195
|
## Quick Start
|
|
112
196
|
|
|
113
197
|
### Simple Counter Example
|
|
@@ -209,8 +293,8 @@ const entities = {
|
|
|
209
293
|
const store = createStore({ types, entities })
|
|
210
294
|
|
|
211
295
|
// 4. Create selectors
|
|
212
|
-
const selectTasks = (
|
|
213
|
-
const selectActiveFilter = (
|
|
296
|
+
const selectTasks = (state) => state.list.tasks
|
|
297
|
+
const selectActiveFilter = (state) => state.footer.activeFilter
|
|
214
298
|
|
|
215
299
|
const selectFilteredTasks = createSelector(
|
|
216
300
|
[selectTasks, selectActiveFilter],
|
|
@@ -234,10 +318,10 @@ store.subscribe(() => {
|
|
|
234
318
|
// 6. Dispatch events (use notify or dispatch - both work!)
|
|
235
319
|
store.notify("inputChange", "Buy milk")
|
|
236
320
|
store.notify("formSubmit", store.getState().form.value)
|
|
237
|
-
store.notify("toggleClick", 1) // Only
|
|
321
|
+
store.notify("toggleClick", 1) // Only task with id=1 will respond
|
|
238
322
|
store.notify("filterClick", "active")
|
|
239
323
|
|
|
240
|
-
// 7. Process event queue
|
|
324
|
+
// 7. Process event queue (in eager mode this happens automatically)
|
|
241
325
|
store.update()
|
|
242
326
|
```
|
|
243
327
|
|
|
@@ -249,7 +333,7 @@ store.update()
|
|
|
249
333
|
|
|
250
334
|
**This is not OOP with methodsโit's a pub/sub (publish/subscribe) event system.**
|
|
251
335
|
|
|
252
|
-
When you call `store.notify('toggle', '
|
|
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.
|
|
253
337
|
|
|
254
338
|
```javascript
|
|
255
339
|
const todoType = {
|
|
@@ -261,7 +345,7 @@ const todoType = {
|
|
|
261
345
|
}
|
|
262
346
|
|
|
263
347
|
// This broadcasts 'toggle' to all entities
|
|
264
|
-
store.notify("toggle", "
|
|
348
|
+
store.notify("toggle", "todo1") // Only todo1 actually updates
|
|
265
349
|
```
|
|
266
350
|
|
|
267
351
|
**Why this matters:**
|
|
@@ -355,6 +439,7 @@ store.notify("applyDiscount", { id: "item1", percent: 10 })
|
|
|
355
439
|
store.dispatch({ type: "increment", payload: "counter1" })
|
|
356
440
|
|
|
357
441
|
// Process the queue - this is when handlers actually run
|
|
442
|
+
// (In eager mode, this happens automatically)
|
|
358
443
|
store.update()
|
|
359
444
|
```
|
|
360
445
|
|
|
@@ -362,20 +447,106 @@ store.update()
|
|
|
362
447
|
|
|
363
448
|
### Systems (Optional)
|
|
364
449
|
|
|
365
|
-
|
|
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.
|
|
451
|
+
|
|
452
|
+
**When you need a system:**
|
|
453
|
+
|
|
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
|
|
457
|
+
|
|
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.
|
|
461
|
+
|
|
462
|
+
```javascript
|
|
463
|
+
const types = {
|
|
464
|
+
item: {
|
|
465
|
+
addToInventory(item, newItemData) {
|
|
466
|
+
// Individual items don't know about other items
|
|
467
|
+
// Can't check total weight here!
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
|
|
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
|
+
}
|
|
489
|
+
|
|
490
|
+
// Add the new item
|
|
491
|
+
const newId = `item${Date.now()}`
|
|
492
|
+
state[newId] = {
|
|
493
|
+
id: newId,
|
|
494
|
+
type: "item",
|
|
495
|
+
...newItemData,
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
]
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Why this needs a system:**
|
|
503
|
+
|
|
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
|
|
508
|
+
|
|
509
|
+
**Another example: Multiplayer Turn System**
|
|
366
510
|
|
|
367
511
|
```javascript
|
|
368
512
|
const systems = [
|
|
369
513
|
{
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
}
|
|
374
536
|
},
|
|
375
537
|
},
|
|
376
538
|
]
|
|
377
539
|
```
|
|
378
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
|
+
|
|
379
550
|
---
|
|
380
551
|
|
|
381
552
|
## API Reference
|
|
@@ -389,6 +560,8 @@ Creates a new store instance.
|
|
|
389
560
|
- `types` (object): Map of type names to behaviors (single object or array)
|
|
390
561
|
- `entities` (object): Initial entities by ID
|
|
391
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
|
|
392
565
|
|
|
393
566
|
**Returns:**
|
|
394
567
|
|
|
@@ -396,6 +569,7 @@ Creates a new store instance.
|
|
|
396
569
|
- `update(dt)`: Process event queue (optional `dt` for time-based logic)
|
|
397
570
|
- `notify(type, payload)`: Queue an event
|
|
398
571
|
- `dispatch(event)`: Redux-compatible event dispatch
|
|
572
|
+
- `getTypes()`: Returns the augmented types configuration
|
|
399
573
|
- `getState()`: Get current immutable state
|
|
400
574
|
- `setState(newState)`: Replace entire state
|
|
401
575
|
- `reset()`: Reset to initial state
|
|
@@ -415,8 +589,9 @@ Creates a convenience wrapper with utility methods.
|
|
|
415
589
|
Create memoized, performant selectors.
|
|
416
590
|
|
|
417
591
|
```javascript
|
|
418
|
-
const selectCompletedTasks = createSelector(
|
|
419
|
-
|
|
592
|
+
const selectCompletedTasks = createSelector(
|
|
593
|
+
[(state) => state.list.tasks],
|
|
594
|
+
(tasks) => tasks.filter((task) => task.completed),
|
|
420
595
|
)
|
|
421
596
|
```
|
|
422
597
|
|
|
@@ -426,39 +601,42 @@ const selectCompletedTasks = createSelector([(state) => state.tasks], (tasks) =>
|
|
|
426
601
|
|
|
427
602
|
### โ
Perfect For
|
|
428
603
|
|
|
429
|
-
- **
|
|
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)
|
|
430
607
|
- **Chat and messaging apps**
|
|
431
608
|
- **Live dashboards and monitoring**
|
|
432
609
|
- **Interactive data visualizations**
|
|
433
610
|
- **Apps with undo/redo**
|
|
434
|
-
- **Multiplayer features**
|
|
435
611
|
- **Collection-based UIs** (lists, feeds, boards)
|
|
436
612
|
- **...and games!**
|
|
437
613
|
|
|
438
614
|
### ๐ค Maybe Overkill For
|
|
439
615
|
|
|
440
|
-
- Simple forms with local state
|
|
616
|
+
- Simple forms with local state only
|
|
441
617
|
- Static marketing pages
|
|
442
|
-
-
|
|
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.
|
|
443
621
|
|
|
444
622
|
---
|
|
445
623
|
|
|
446
624
|
## Comparison
|
|
447
625
|
|
|
448
|
-
| Feature | Inglorious Store | Redux
|
|
449
|
-
| --------------------------- | ----------------- |
|
|
450
|
-
| **Integrated Immutability** | โ
Mutative | โ Manual
|
|
451
|
-
| **Event Queue/Batching** | โ
Built-in | โ
|
|
452
|
-
| **Dispatch from Handlers** | โ
Safe (queued) | โ Not allowed
|
|
453
|
-
| **Redux DevTools** | โ ๏ธ Limited | โ
Native
|
|
454
|
-
| **react-redux Compatible** | โ
Yes | โ
Yes
|
|
455
|
-
| **Time-Travel Debug** | โ
Built-in | โ
Via DevTools
|
|
456
|
-
| **Entity-Based State** | โ
First-class | โ ๏ธ Manual
|
|
457
|
-
| **Pub/Sub Events** | โ
Core pattern | โ
|
|
458
|
-
| **Multiplayer-Ready** | โ
Deterministic | โ ๏ธ With work
|
|
459
|
-
| **Testability** | โ
Pure functions | โ
Pure reducers
|
|
460
|
-
| **Learning Curve** | Medium | High
|
|
461
|
-
| **Bundle Size** | Small | Small
|
|
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 |
|
|
462
640
|
|
|
463
641
|
### Key Differences
|
|
464
642
|
|
|
@@ -519,25 +697,59 @@ const selectCompletedTasks = createSelector([(state) => state.tasks], (tasks) =>
|
|
|
519
697
|
|
|
520
698
|
## Advanced: Real-Time Sync
|
|
521
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
|
+
```
|
|
708
|
+
|
|
709
|
+
### Step 2: Add WebSocket (literally ~10 lines)
|
|
710
|
+
|
|
522
711
|
```javascript
|
|
523
|
-
//
|
|
524
|
-
socket.on("
|
|
712
|
+
// Receive events from other clients
|
|
713
|
+
socket.on("remote-event", (event) => {
|
|
525
714
|
store.notify(event.type, event.payload)
|
|
526
|
-
store.update()
|
|
527
715
|
})
|
|
528
716
|
|
|
529
|
-
// Send
|
|
530
|
-
store.
|
|
531
|
-
|
|
532
|
-
socket.emit("
|
|
717
|
+
// Send your events to other clients
|
|
718
|
+
const processedEvents = store.update()
|
|
719
|
+
processedEvents.forEach((event) => {
|
|
720
|
+
socket.emit("event", event)
|
|
533
721
|
})
|
|
534
722
|
```
|
|
535
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
|
+
```
|
|
745
|
+
|
|
746
|
+
This is exactly how multiplayer games work. Now your app can too.
|
|
747
|
+
|
|
536
748
|
---
|
|
537
749
|
|
|
538
750
|
## Advanced: Time-Based Updates
|
|
539
751
|
|
|
540
|
-
For animations or
|
|
752
|
+
For animations, games, or any time-dependent logic, you can run a continuous update loop:
|
|
541
753
|
|
|
542
754
|
```javascript
|
|
543
755
|
const types = {
|
|
@@ -553,14 +765,51 @@ const types = {
|
|
|
553
765
|
],
|
|
554
766
|
}
|
|
555
767
|
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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()
|
|
560
774
|
requestAnimationFrame(loop)
|
|
561
775
|
}
|
|
776
|
+
loop()
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
**For typical apps (todos, forms, dashboards):** Use eager mode (default). No loop needed.
|
|
780
|
+
|
|
781
|
+
**For real-time apps (games, animations, live data):** Use batched mode with a loop for smooth, consistent updates.
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## The Path from Solo to Multiplayer
|
|
786
|
+
|
|
787
|
+
### Week 1: Build a simple todo app
|
|
788
|
+
|
|
789
|
+
```javascript
|
|
790
|
+
store.notify("addTodo", { text: "Buy milk" })
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
_Works great. Clean architecture. Nothing fancy._
|
|
794
|
+
|
|
795
|
+
### Month 6: Users love it, ask for undo/redo
|
|
796
|
+
|
|
797
|
+
```javascript
|
|
798
|
+
const snapshot = store.getState()
|
|
799
|
+
// ... user makes changes ...
|
|
800
|
+
store.setState(snapshot) // Undo!
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
_Already built-in. No refactoring needed._
|
|
804
|
+
|
|
805
|
+
### Year 1: Competitor launches with real-time collaboration
|
|
806
|
+
|
|
807
|
+
```javascript
|
|
808
|
+
socket.on("remote-event", (e) => store.notify(e.type, e.payload))
|
|
562
809
|
```
|
|
563
810
|
|
|
811
|
+
_Add multiplayer in an afternoon. You win._
|
|
812
|
+
|
|
564
813
|
---
|
|
565
814
|
|
|
566
815
|
## Part of the Inglorious Engine
|
|
@@ -569,11 +818,22 @@ This store powers the [Inglorious Engine](https://github.com/IngloriousCoderz/in
|
|
|
569
818
|
|
|
570
819
|
---
|
|
571
820
|
|
|
821
|
+
## What's Next?
|
|
822
|
+
|
|
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
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
572
830
|
## License
|
|
573
831
|
|
|
574
|
-
MIT
|
|
832
|
+
**MIT License - Free and open source**
|
|
833
|
+
|
|
834
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
575
835
|
|
|
576
|
-
|
|
836
|
+
You're free to use, modify, and distribute this software. See [LICENSE](../../LICENSE) for details.
|
|
577
837
|
|
|
578
838
|
---
|
|
579
839
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.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",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"mutative": "^1.3.0",
|
|
45
|
-
"@inglorious/utils": "3.6.
|
|
45
|
+
"@inglorious/utils": "3.6.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@inglorious/utils": "3.6.
|
|
48
|
+
"@inglorious/utils": "3.6.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"prettier": "^3.6.2",
|
package/src/store.js
CHANGED
|
@@ -11,6 +11,9 @@ import { augmentType, augmentTypes } from "./types.js"
|
|
|
11
11
|
* @param {Object} config - Configuration options for the store.
|
|
12
12
|
* @param {Object} [config.types] - The initial types configuration.
|
|
13
13
|
* @param {Object} [config.entities] - The initial entities configuration.
|
|
14
|
+
* @param {Array} [config.systens] - The initial systems configuration.
|
|
15
|
+
* @param {Array} [config.middlewares] - The initial middlewares configuration.
|
|
16
|
+
* @param {"eager" | "batched"} [config.mode] - The dispatch mode (defaults to "eager").
|
|
14
17
|
* @returns {Object} The store with methods to interact with state and events.
|
|
15
18
|
*/
|
|
16
19
|
export function createStore({
|
|
@@ -18,6 +21,7 @@ export function createStore({
|
|
|
18
21
|
entities: originalEntities,
|
|
19
22
|
systems = [],
|
|
20
23
|
middlewares = [],
|
|
24
|
+
mode = "eager",
|
|
21
25
|
}) {
|
|
22
26
|
const listeners = new Set()
|
|
23
27
|
|
|
@@ -33,7 +37,6 @@ export function createStore({
|
|
|
33
37
|
dispatch, // needed for compatibility with Redux
|
|
34
38
|
getApi,
|
|
35
39
|
getTypes,
|
|
36
|
-
getOriginalTypes,
|
|
37
40
|
getState,
|
|
38
41
|
setState,
|
|
39
42
|
reset,
|
|
@@ -146,6 +149,9 @@ export function createStore({
|
|
|
146
149
|
*/
|
|
147
150
|
function dispatch(event) {
|
|
148
151
|
incomingEvents.push(event)
|
|
152
|
+
if (mode === "eager") {
|
|
153
|
+
update()
|
|
154
|
+
}
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
/**
|
|
@@ -165,14 +171,6 @@ export function createStore({
|
|
|
165
171
|
return types
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
/**
|
|
169
|
-
* Retrieves the original, un-augmented types configuration.
|
|
170
|
-
* @returns {Object} The original types configuration.
|
|
171
|
-
*/
|
|
172
|
-
function getOriginalTypes() {
|
|
173
|
-
return originalTypes
|
|
174
|
-
}
|
|
175
|
-
|
|
176
174
|
/**
|
|
177
175
|
* Retrieves the current state.
|
|
178
176
|
* @returns {Object} The current state.
|
package/src/store.test.js
CHANGED
|
@@ -43,6 +43,7 @@ test("it should process an event queue in the same update cycle", () => {
|
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
|
+
|
|
46
47
|
entities: {
|
|
47
48
|
kitty1: { type: "kitty" },
|
|
48
49
|
},
|
|
@@ -65,7 +66,7 @@ test("it should process an event queue in the same update cycle", () => {
|
|
|
65
66
|
expect(state).toStrictEqual(afterState)
|
|
66
67
|
})
|
|
67
68
|
|
|
68
|
-
test("it should send an event from an entity and process it in the same update cycle", () => {
|
|
69
|
+
test("it should send an event from an entity and process it in the same update cycle in batched mode", () => {
|
|
69
70
|
const config = {
|
|
70
71
|
types: {
|
|
71
72
|
doggo: {
|
|
@@ -79,10 +80,13 @@ test("it should send an event from an entity and process it in the same update c
|
|
|
79
80
|
},
|
|
80
81
|
},
|
|
81
82
|
},
|
|
83
|
+
|
|
82
84
|
entities: {
|
|
83
85
|
doggo1: { type: "doggo" },
|
|
84
86
|
kitty1: { type: "kitty", position: "near" },
|
|
85
87
|
},
|
|
88
|
+
|
|
89
|
+
mode: "batched",
|
|
86
90
|
}
|
|
87
91
|
const afterState = {
|
|
88
92
|
doggo1: { id: "doggo1", type: "doggo" },
|