@inglorious/store 9.0.1 → 9.2.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/LICENSE CHANGED
@@ -1,9 +1,9 @@
1
- The MIT License (MIT)
2
-
3
- Copyright © 2025 Inglorious Coderz Srl.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
-
7
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
-
9
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2025 Inglorious Coderz Srl.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -651,6 +651,125 @@ Instead of re-rendering after each event, you can batch them and re-render once.
651
651
 
652
652
  ---
653
653
 
654
+ ## 🧮 Derived State with `compute`
655
+
656
+ Inglorious Store is fully compatible with Redux selectors, but it also provides a simpler, more explicit primitive for derived state: **`compute`**.
657
+
658
+ `compute` lets you derive values from the store state in a predictable and memoized way, without hidden reactivity or dependency graphs.
659
+
660
+ ### Basic example
661
+
662
+ ```js
663
+ import { compute } from "@inglorious/store"
664
+
665
+ const selectCounter = (state) => state.counter1.value
666
+ const selectMultiplier = (state) => state.settings.multiplier
667
+
668
+ const selectResult = compute(
669
+ (count, multiplier) => count * multiplier,
670
+ [selectCounter, selectMultiplier],
671
+ )
672
+ ```
673
+
674
+ The returned function is a standard selector:
675
+
676
+ ```js
677
+ const result = selectResult(api.getEntities())
678
+ ```
679
+
680
+ And it works seamlessly with `react-redux` or `@inglorious/react-store`:
681
+
682
+ ```js
683
+ const value = useSelector(selectResult)
684
+ ```
685
+
686
+ ---
687
+
688
+ ### How `compute` works
689
+
690
+ - Each **input selector** is called with the current state
691
+ - The results are compared using **strict equality (`===`)**
692
+ - If none of the inputs changed, the **previous result is reused**
693
+ - If at least one input changed, the result is recomputed
694
+
695
+ There is:
696
+
697
+ - ❌ no deep comparison
698
+ - ❌ no proxy-based reactivity
699
+ - ❌ no implicit dependency tracking
700
+
701
+ Memoization is **explicit and predictable**, based purely on the values returned by your selectors.
702
+
703
+ ---
704
+
705
+ ### Why `compute` instead of magic reactivity?
706
+
707
+ `compute` matches the core philosophy of Inglorious Store:
708
+
709
+ - derived state is just a function
710
+ - updates are explicit
711
+ - behavior is easy to reason about
712
+ - performance characteristics are obvious
713
+
714
+ If an input selector returns a new reference, the computation runs again.
715
+ If it doesn’t, it won’t.
716
+
717
+ No surprises.
718
+
719
+ ---
720
+
721
+ ### Zero or single input selectors
722
+
723
+ `compute` supports any number of inputs — including zero:
724
+
725
+ ```js
726
+ const selectConstant = compute(() => 42)
727
+ ```
728
+
729
+ Or just one:
730
+
731
+ ```js
732
+ const selectDouble = compute(
733
+ (count) => count * 2,
734
+ [(entities) => entities.counter1.value],
735
+ )
736
+ ```
737
+
738
+ ---
739
+
740
+ ### `createSelector` (Redux compatibility)
741
+
742
+ For migration and familiarity, Inglorious Store also exports `createSelector`, which is fully compatible with Redux:
743
+
744
+ ```js
745
+ import { createSelector } from "@inglorious/store"
746
+
747
+ const selectResult = createSelector(
748
+ [(state) => state.counter1.value],
749
+ (count) => count * 2,
750
+ )
751
+ ```
752
+
753
+ Internally, `createSelector` is just an alias for `compute`.
754
+
755
+ > **Note:** `compute` is the preferred API for new code.
756
+ > `createSelector` exists mainly for Redux compatibility and migration.
757
+
758
+ ---
759
+
760
+ ### When to use `compute`
761
+
762
+ Use `compute` when:
763
+
764
+ - you need derived or aggregated state
765
+ - you want memoization without magic
766
+ - you want selectors that are easy to test
767
+ - you want predictable recomputation rules
768
+
769
+ If you already know Redux selectors, you already know how to use `compute` — just with fewer rules and less ceremony.
770
+
771
+ ---
772
+
654
773
  ## Comparison with Other State Libraries
655
774
 
656
775
  | Feature | Redux | RTK | Zustand | Jotai | Pinia | MobX | Inglorious Store |
@@ -675,12 +794,75 @@ const store = createStore({
675
794
  types, // Object: entity type definitions
676
795
  entities, // Object: initial entities
677
796
  systems, // Array (optional): global state handlers
797
+ autoCreateEntities, // Boolean (optional): false (default) or true
678
798
  updateMode, // String (optional): 'auto' (default) or 'manual'
679
799
  })
680
800
  ```
681
801
 
682
802
  **Returns:** A Redux-compatible store
683
803
 
804
+ **Options:**
805
+
806
+ - **`types`** (required) - Object defining entity type behaviors
807
+ - **`entities`** (required) - Object containing initial entity instances
808
+ - **`systems`** (optional) - Array of global state handlers
809
+ - **`autoCreateEntities`** (optional) - Automatically create singleton entities for types not defined in `entities`:
810
+ - `false` (default) - Only use explicitly defined entities
811
+ - `true` - Auto-create entities matching their type name
812
+ - **`updateMode`** (optional) - Controls when React components re-render:
813
+ - `'auto'` (default) - Automatic updates after each event
814
+ - `'manual'` - Manual control via `api.update()`
815
+
816
+ #### Auto-Create Entities
817
+
818
+ When `autoCreateEntities: true`, the store automatically creates singleton entities for any type that doesn't have a corresponding entity defined. This is particularly useful for singleton-type entities that behave like components, eliminating the need to switch between type definitions and entity declarations.
819
+
820
+ ```javascript
821
+ const types = {
822
+ settings: {
823
+ setTheme(entity, theme) {
824
+ entity.theme = theme
825
+ },
826
+ },
827
+ analytics: {
828
+ track(entity, event) {
829
+ entity.events.push(event)
830
+ },
831
+ },
832
+ }
833
+
834
+ // Without autoCreateEntities (default)
835
+ const entities = {
836
+ settings: { type: "settings", theme: "dark" },
837
+ analytics: { type: "analytics", events: [] },
838
+ }
839
+
840
+ // With autoCreateEntities: true
841
+ const entities = {
842
+ // settings and analytics will be auto-created as:
843
+ // settings: { type: "settings" }
844
+ // analytics: { type: "analytics" }
845
+ }
846
+
847
+ const store = createStore({
848
+ types,
849
+ entities,
850
+ autoCreateEntities: true,
851
+ })
852
+
853
+ // Both approaches work the same way
854
+ store.notify("settings:setTheme", "light")
855
+ store.notify("analytics:track", { action: "click" })
856
+ ```
857
+
858
+ **When to use `autoCreateEntities`:**
859
+
860
+ - ✅ Building web applications with singleton services (settings, auth, analytics)
861
+ - ✅ Component-like entities that only need one instance
862
+ - ✅ Rapid prototyping where you want to add types without ceremony
863
+ - ❌ Game development with multiple entity instances (players, enemies, items)
864
+ - ❌ When you need fine control over initial entity state
865
+
684
866
  ### Types Definition
685
867
 
686
868
  ```javascript
@@ -733,6 +915,8 @@ store.notify("eventName", payload)
733
915
  store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
734
916
  ```
735
917
 
918
+ ---
919
+
736
920
  ### 🧩 Type Safety
737
921
 
738
922
  Inglorious Store is written in JavaScript but comes with powerful TypeScript support out of the box, allowing for a fully type-safe experience similar to Redux Toolkit, but with less boilerplate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "9.0.1",
3
+ "version": "9.2.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/select.js CHANGED
@@ -1,17 +1,43 @@
1
1
  /**
2
- * Creates a memoized selector function.
3
- * NB: this implementation does not support spreading the input selectors for clarity, please just put them in an array.
4
- * @param {Array<Function>} inputSelectors - An array of input selector functions.
5
- * @param {Function} resultFunc - A function that receives the results of the input selectors and returns a computed value.
6
- * @returns {Function} A memoized selector function that, when called, returns the selected state.
2
+ * @typedef {import('../types/select').InputSelector} InputSelector
3
+ * @typedef {import('../types/select').OutputSelector} OutputSelector
7
4
  */
8
- export function createSelector(inputSelectors, resultFunc) {
9
- let lastInputs = []
10
- let lastResult = null
5
+
6
+ /**
7
+ * Creates a memoized derived computation from one or more input selectors.
8
+ *
9
+ * `compute` returns a function that, given the application state, will:
10
+ * - run each input selector with the state
11
+ * - compare the results with the previous invocation using strict equality
12
+ * - recompute the result only if at least one input has changed
13
+ *
14
+ * This is a simple, explicit memoization utility.
15
+ * There is no dependency tracking, deep comparison, or reactive graph:
16
+ * memoization is based solely on referential equality of selector outputs.
17
+ *
18
+ * @template TState
19
+ * @template TResult
20
+ *
21
+ * @param {(…inputs: any[]) => TResult} resultFunc
22
+ * A function that receives the results of the input selectors and returns
23
+ * the computed value.
24
+ *
25
+ * @param {InputSelector<TState, any>[]} inputSelectors
26
+ * An array of selector functions used to extract inputs from the state (optional).
27
+ *
28
+ * @returns {OutputSelector<TState, TResult>}
29
+ * A memoized function that computes and returns the derived value.
30
+ */
31
+ export function compute(resultFunc, inputSelectors = []) {
32
+ let lastInputs
33
+ let lastResult
34
+ let initialized = false
11
35
 
12
36
  return (state) => {
13
37
  const nextInputs = inputSelectors.map((selector) => selector(state))
38
+
14
39
  const inputsChanged =
40
+ !initialized ||
15
41
  lastInputs.length !== nextInputs.length ||
16
42
  nextInputs.some((input, index) => input !== lastInputs[index])
17
43
 
@@ -21,6 +47,30 @@ export function createSelector(inputSelectors, resultFunc) {
21
47
 
22
48
  lastInputs = nextInputs
23
49
  lastResult = resultFunc(...nextInputs)
50
+ initialized = true
24
51
  return lastResult
25
52
  }
26
53
  }
54
+
55
+ /**
56
+ * Redux-compatible alias for {@link compute}.
57
+ *
58
+ * This function exists for familiarity and migration purposes.
59
+ * Prefer using `compute` directly in new code.
60
+ *
61
+ * @template TState
62
+ * @template TResult
63
+ *
64
+ * @param {InputSelector<TState, any>[]} inputSelectors
65
+ * An array of input selector functions.
66
+ *
67
+ * @param {(…inputs: any[]) => TResult} resultFunc
68
+ * A function that receives the results of the input selectors and returns
69
+ * a computed value.
70
+ *
71
+ * @returns {OutputSelector<TState, TResult>}
72
+ * A memoized selector function.
73
+ */
74
+ export function createSelector(inputSelectors, resultFunc) {
75
+ return compute(resultFunc, inputSelectors)
76
+ }
package/src/store.js CHANGED
@@ -13,6 +13,7 @@ import { augmentType, augmentTypes } from "./types.js"
13
13
  * @param {Object} [config.entities] - The initial entities configuration.
14
14
  * @param {Array} [config.systems] - The initial systems configuration.
15
15
  * @param {Array} [config.middlewares] - The initial middlewares configuration.
16
+ * @param {boolean} [config.autoCreateEntities] - Creates entities if not defined in `config.entities`.
16
17
  * @param {"auto" | "manual"} [config.updateMode] - The update mode (defaults to "auto").
17
18
  * @returns {Object} The store with methods to interact with state and events.
18
19
  */
@@ -21,6 +22,7 @@ export function createStore({
21
22
  entities: originalEntities = {},
22
23
  systems = [],
23
24
  middlewares = [],
25
+ autoCreateEntities = false,
24
26
  updateMode = "auto",
25
27
  } = {}) {
26
28
  const listeners = new Set()
@@ -220,8 +222,25 @@ export function createStore({
220
222
  const oldEntities = state ?? {}
221
223
  const newEntities = augmentEntities(nextState)
222
224
 
225
+ if (autoCreateEntities) {
226
+ for (const typeName of Object.keys(types)) {
227
+ // Check if entity already exists
228
+ const hasEntity = Object.values(newEntities).some(
229
+ (entity) => entity.type === typeName,
230
+ )
231
+
232
+ if (!hasEntity) {
233
+ // No entity for this type → auto-create minimal entity
234
+ newEntities[typeName] = {
235
+ id: typeName,
236
+ type: typeName,
237
+ }
238
+ }
239
+ }
240
+ }
241
+
223
242
  state = newEntities
224
- eventMap = new EventMap(types, nextState)
243
+ eventMap = new EventMap(types, newEntities)
225
244
  incomingEvents = []
226
245
  isProcessing = false
227
246
 
@@ -0,0 +1,55 @@
1
+ import type { BaseEntity, EntitiesState, Middleware } from "../store"
2
+
3
+ /**
4
+ * A generic event dispatched through the store.
5
+ */
6
+ export interface MultiplayerEvent {
7
+ type: string
8
+ payload?: unknown
9
+ fromServer?: boolean
10
+ [key: string]: unknown
11
+ }
12
+
13
+ /**
14
+ * Configuration options for the multiplayer middleware.
15
+ */
16
+ export interface MultiplayerMiddlewareConfig {
17
+ /**
18
+ * WebSocket server URL.
19
+ * Defaults to ws://<current-hostname>:3000
20
+ */
21
+ serverUrl?: string
22
+
23
+ /**
24
+ * Delay (in milliseconds) before attempting to reconnect
25
+ * after the WebSocket connection is closed.
26
+ *
27
+ * @default 1000
28
+ */
29
+ reconnectionDelay?: number
30
+
31
+ /**
32
+ * Event types that should NOT be sent to the server.
33
+ */
34
+ blacklist?: string[]
35
+
36
+ /**
37
+ * Event types that SHOULD be sent to the server.
38
+ * If provided, only these events are sent.
39
+ */
40
+ whitelist?: string[]
41
+
42
+ /**
43
+ * Custom predicate to decide whether an event
44
+ * should be sent to the server.
45
+ */
46
+ filter?: (event: MultiplayerEvent) => boolean
47
+ }
48
+
49
+ /**
50
+ * Creates the multiplayer middleware.
51
+ */
52
+ export function multiplayerMiddleware<
53
+ T extends BaseEntity = BaseEntity,
54
+ S extends EntitiesState<T> = EntitiesState<T>,
55
+ >(config?: MultiplayerMiddlewareConfig): Middleware<T, S>
package/types/select.d.ts CHANGED
@@ -13,12 +13,26 @@ export type OutputSelector<TState = any, TResult = any> = (
13
13
  ) => TResult
14
14
 
15
15
  /**
16
- * Creates a memoized selector function.
17
- * @param inputSelectors - An array of input selector functions.
18
- * @param resultFunc - A function that receives the results of the input selectors and returns a computed value.
19
- * @returns A memoized selector function that, when called, returns the selected state.
16
+ * Creates a memoized derived computation from input selectors.
20
17
  */
21
- export function createSelector<TState = any, TResult = any>(
22
- inputSelectors: InputSelector<TState, any>[],
23
- resultFunc: (...args: any[]) => TResult,
18
+ export function compute<TState, TInputs extends readonly unknown[], TResult>(
19
+ resultFunc: (...args: TInputs) => TResult,
20
+ inputSelectors: {
21
+ [K in keyof TInputs]: InputSelector<TState, TInputs[K]>
22
+ },
23
+ ): OutputSelector<TState, TResult>
24
+
25
+ /**
26
+ * Redux-compatible alias for `compute`.
27
+ * Prefer `compute` in new code.
28
+ */
29
+ export function createSelector<
30
+ TState,
31
+ TInputs extends readonly unknown[],
32
+ TResult,
33
+ >(
34
+ inputSelectors: {
35
+ [K in keyof TInputs]: InputSelector<TState, TInputs[K]>
36
+ },
37
+ resultFunc: (...args: TInputs) => TResult,
24
38
  ): OutputSelector<TState, TResult>
package/types/store.d.ts CHANGED
@@ -53,6 +53,7 @@ export interface StoreConfig<
53
53
  entities?: TState
54
54
  systems?: System<TState>[]
55
55
  middlewares?: Middleware<TEntity, TState>[]
56
+ autoCreateEntities?: boolean
56
57
  mode?: "eager" | "batched"
57
58
  }
58
59