@inglorious/store 7.0.0 → 7.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -477,6 +477,7 @@ Notice: you don't need pending/fulfilled/rejected actions. You stay in control o
477
477
  - **`api.notify(type, payload)`** - trigger other events (queued, not immediate)
478
478
  - **`api.dispatch(action)`** - optional, if you prefer Redux-style dispatching
479
479
  - **`api.getTypes()`** - access type definitions (mainly for middleware/plugins)
480
+ - **`api.getType(typeName)`** - access type definition (mainly for overrides)
480
481
 
481
482
  All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
482
483
 
@@ -635,10 +636,10 @@ When multiple behaviors define the same event, they all run in order. This allow
635
636
 
636
637
  ### ⏱️ Batched Mode
637
638
 
638
- The Inglorious Store features an **event queue**. In the default `eager` mode, each notified event will trigger and update of the state (same as Redux). But in `batched` mode, you can process multiple events together before re-rendering:
639
+ The Inglorious Store features an **event queue**. In the default `auto` update mode, each notified event will trigger and update of the state (same as Redux). But in `manual` update mode, you can process multiple events together before re-rendering:
639
640
 
640
641
  ```javascript
641
- const store = createStore({ types, entities, mode: "batched" })
642
+ const store = createStore({ types, entities, updateMode: "manual" })
642
643
 
643
644
  // add events to the event queue
644
645
  store.notify("playerMoved", { x: 100, y: 50 })
@@ -677,7 +678,7 @@ const store = createStore({
677
678
  types, // Object: entity type definitions
678
679
  entities, // Object: initial entities
679
680
  systems, // Array (optional): global state handlers
680
- mode, // String (optional): 'eager' (default) or 'batched'
681
+ updateMode, // String (optional): 'auto' (default) or 'manual'
681
682
  })
682
683
  ```
683
684
 
@@ -718,6 +719,7 @@ Each handler receives three arguments:
718
719
  - `notify(type, payload)` - trigger other events
719
720
  - `dispatch(action)` - optional, if you prefer Redux-style dispatching
720
721
  - `getTypes()` - type definitions (for middleware)
722
+ - `getType(typeName)` - type definition (for overriding)
721
723
 
722
724
  ### Built-in Lifecycle Events
723
725
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "7.0.0",
3
+ "version": "7.1.1",
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",
@@ -58,7 +58,7 @@
58
58
  "prettier": "^3.6.2",
59
59
  "vite": "^7.1.3",
60
60
  "vitest": "^1.6.1",
61
- "@inglorious/eslint-config": "1.0.1"
61
+ "@inglorious/eslint-config": "1.1.0"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">= 22"
package/src/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export function createApi(store, extras) {
2
2
  return {
3
3
  getTypes: store.getTypes,
4
+ getType: store.getType,
4
5
  getEntities: store.getState,
5
6
  getEntity: (id) => store.getState()[id],
6
7
  dispatch: store.dispatch,
@@ -12,7 +12,7 @@ const globalContainer = import.meta.hot
12
12
  // devToolsInstance,
13
13
  // unsubscribe,
14
14
  // store,
15
- // mode,
15
+ // updateMode,
16
16
  // restoreSetState: () => void,
17
17
  // }
18
18
 
@@ -45,18 +45,18 @@ export function createDevtools(config = {}) {
45
45
  }
46
46
 
47
47
  function connectDevTools(store, config = {}) {
48
- const mode = config.mode ?? "eager"
48
+ const updateMode = config.updateMode ?? "auto"
49
49
 
50
- // Prevent duplicate connections; rewire if store or mode changed
50
+ // Prevent duplicate connections; rewire if store or update mode changed
51
51
  const existing = getConnection()
52
52
  if (existing) {
53
- const sameMode = existing.mode === mode
53
+ const sameMode = existing.updateMode === updateMode
54
54
  if (sameMode) {
55
55
  // Hot-swap store without resetting DevTools history
56
56
  existing.restoreSetState?.()
57
57
  const baseSetState = store.setState
58
58
  let restoreSetState = null
59
- if (mode === "eager") {
59
+ if (updateMode === "auto") {
60
60
  restoreSetState = () => {
61
61
  if (store.setState !== baseSetState) {
62
62
  store.setState = baseSetState
@@ -75,7 +75,7 @@ function connectDevTools(store, config = {}) {
75
75
  existing.restoreSetState = restoreSetState
76
76
  }
77
77
 
78
- // Different mode: fully disconnect previous and proceed
78
+ // Different update mode: fully disconnect previous and proceed
79
79
  try {
80
80
  existing.unsubscribe?.()
81
81
  } finally {
@@ -91,8 +91,8 @@ function connectDevTools(store, config = {}) {
91
91
  const name = config.name ?? document.title
92
92
  const baseSetState = store.setState
93
93
  let restoreSetState = null
94
- // Only add setState side-effects in eager mode; batched mode logs explicitly from engine.
95
- if (mode === "eager") {
94
+ // Only add setState side-effects in auto update mode; manual update mode logs explicitly from engine.
95
+ if (updateMode === "auto") {
96
96
  restoreSetState = () => {
97
97
  if (store.setState !== baseSetState) {
98
98
  store.setState = baseSetState
@@ -140,7 +140,13 @@ function connectDevTools(store, config = {}) {
140
140
 
141
141
  devToolsInstance.init(store.getState())
142
142
 
143
- setConnection({ devToolsInstance, unsubscribe, store, mode, restoreSetState })
143
+ setConnection({
144
+ devToolsInstance,
145
+ unsubscribe,
146
+ store,
147
+ updateMode,
148
+ restoreSetState,
149
+ })
144
150
  }
145
151
 
146
152
  function disconnectDevTools() {
@@ -234,12 +240,12 @@ function handleAction(message) {
234
240
 
235
241
  function shouldLogEvent(event, config) {
236
242
  const {
237
- mode = "eager",
243
+ updateMode = "auto",
238
244
  blacklist = [],
239
245
  whitelist = [],
240
246
  filter = null,
241
247
  } = config
242
- if (mode !== "eager") return false
248
+ if (updateMode !== "auto") return false
243
249
  const passesBlacklist = !blacklist.length || !blacklist.includes(event.type)
244
250
  const passesWhitelist = !whitelist.length || whitelist.includes(event.type)
245
251
  const passesFilter = !filter || filter(event)
package/src/event-map.js CHANGED
@@ -3,9 +3,11 @@
3
3
  * @typedef {Object.<string, any>} Entity - An object representing a entity.
4
4
  */
5
5
 
6
+ const SPLIT_LIMIT = 2
7
+
6
8
  /**
7
9
  * A class to manage the mapping of event names to the entity IDs that handle them.
8
- * This is used for optimized event handling.
10
+ * This is used for optimized event handling with support for scoped events.
9
11
  */
10
12
  export class EventMap {
11
13
  /**
@@ -16,17 +18,26 @@ export class EventMap {
16
18
  */
17
19
  constructor(types, entities) {
18
20
  /**
19
- * The internal map where keys are event names and values are Sets of entity IDs.
21
+ * Maps handler names to type names to Sets of entity IDs.
22
+ * Structure: handlerName -> typeName -> Set<entityId>
23
+ * Example: 'submit' -> 'form' -> Set(['loginForm', 'signupForm'])
24
+ *
25
+ * @type {Map<string, Map<string, Set<string>>>}
26
+ */
27
+ this.handlerToTypeToEntities = new Map()
28
+
29
+ /**
30
+ * Maps entity IDs to their type names for quick lookup.
20
31
  *
21
- * @type {Object.<string, Set<string>>}
32
+ * @type {Map<string, string>}
22
33
  */
23
- this.map = {}
34
+ this.entityTypes = new Map()
24
35
 
25
36
  for (const entityId in entities) {
26
37
  const entity = entities[entityId]
27
38
  const type = types[entity.type]
28
39
  if (type) {
29
- this.addEntity(entityId, type)
40
+ this.addEntity(entityId, type, entity.type)
30
41
  }
31
42
  }
32
43
  }
@@ -37,13 +48,24 @@ export class EventMap {
37
48
  *
38
49
  * @param {string} entityId - The ID of the entity.
39
50
  * @param {Type} type - The augmented type object of the entity.
51
+ * @param {string} typeName - The name of the entity's type.
40
52
  */
41
- addEntity(entityId, type) {
42
- for (const eventName in type) {
43
- if (!this.map[eventName]) {
44
- this.map[eventName] = new Set()
53
+ addEntity(entityId, type, typeName) {
54
+ this.entityTypes.set(entityId, typeName)
55
+
56
+ for (const handlerName in type) {
57
+ if (typeof type[handlerName] !== "function") continue
58
+
59
+ if (!this.handlerToTypeToEntities.has(handlerName)) {
60
+ this.handlerToTypeToEntities.set(handlerName, new Map())
61
+ }
62
+
63
+ const typeMap = this.handlerToTypeToEntities.get(handlerName)
64
+ if (!typeMap.has(typeName)) {
65
+ typeMap.set(typeName, new Set())
45
66
  }
46
- this.map[eventName].add(entityId)
67
+
68
+ typeMap.get(typeName).add(entityId)
47
69
  }
48
70
  }
49
71
 
@@ -53,23 +75,77 @@ export class EventMap {
53
75
  *
54
76
  * @param {string} entityId - The ID of the entity.
55
77
  * @param {Type} type - The augmented type object of the entity.
78
+ * @param {string} typeName - The name of the entity's type.
56
79
  */
57
- removeEntity(entityId, type) {
58
- for (const eventName in type) {
59
- const entitySet = this.map[eventName]
60
- if (entitySet) {
61
- entitySet.delete(entityId)
80
+ removeEntity(entityId, type, typeName) {
81
+ this.entityTypes.delete(entityId)
82
+
83
+ for (const handlerName in type) {
84
+ const typeMap = this.handlerToTypeToEntities.get(handlerName)
85
+ if (typeMap) {
86
+ const entitySet = typeMap.get(typeName)
87
+ if (entitySet) {
88
+ entitySet.delete(entityId)
89
+ }
62
90
  }
63
91
  }
64
92
  }
65
93
 
66
94
  /**
67
- * Retrieves the Set of entity IDs that are subscribed to a given event.
95
+ * Retrieves the array of entity IDs that should handle a given event.
96
+ * Supports scoped events:
97
+ * - 'submit' -> all entities with 'submit' handler
98
+ * - 'form:submit' -> all form entities with 'submit' handler
99
+ * - 'form#loginForm:submit' -> only loginForm entity (of type form)
100
+ * - '#loginForm:submit' -> only loginForm entity
68
101
  *
69
- * @param {string} eventName - The name of the event (e.g., 'update', 'fire').
70
- * @returns {Set<string>} A Set of entity IDs. Returns an empty Set if no entities are found.
102
+ * @param {string} eventString - The event string (e.g., 'submit', 'form:submit', 'form[id]:submit')
103
+ * @returns {string[]} An array of entity IDs that should handle this event.
71
104
  */
72
- getEntitiesForEvent(eventName) {
73
- return this.map[eventName] || new Set()
105
+ getEntitiesForEvent(eventString) {
106
+ const {
107
+ type: targetType,
108
+ entityId: targetEntityId,
109
+ event: handlerName,
110
+ } = parseEvent(eventString)
111
+
112
+ const typeMap = this.handlerToTypeToEntities.get(handlerName)
113
+ if (!typeMap) return []
114
+
115
+ if (targetEntityId) {
116
+ const type = targetType ?? this.entityTypes.get(targetEntityId)
117
+ const entitySet = typeMap.get(type)
118
+ return entitySet && entitySet.has(targetEntityId) ? [targetEntityId] : []
119
+ }
120
+
121
+ // form:submit - all entities of this type
122
+ if (targetType) {
123
+ const entitySet = typeMap.get(targetType)
124
+ return entitySet ? Array.from(entitySet) : []
125
+ }
126
+
127
+ // submit - all entities with this handler (broadcast)
128
+ const allEntities = []
129
+ for (const entitySet of typeMap.values()) {
130
+ allEntities.push(...entitySet)
131
+ }
132
+ return allEntities
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Parses an event string into its components.
138
+ * @param {string} eventString - The event string (e.g., 'submit', 'form:submit', 'form#loginForm:submit')
139
+ * @returns {{ type: string|null, entityId: string|null, event: string }}
140
+ */
141
+ export function parseEvent(eventString) {
142
+ const [left, event] = eventString.split(":", SPLIT_LIMIT)
143
+ if (!event) return { type: null, entityId: null, event: left }
144
+
145
+ const [type, entityId] = left.split("#", SPLIT_LIMIT)
146
+ return {
147
+ type: type || null,
148
+ entityId: entityId || null,
149
+ event,
74
150
  }
75
151
  }
@@ -25,17 +25,20 @@ test("constructor should initialize the event map from types and entities", () =
25
25
 
26
26
  const eventMap = new EventMap(types, entities)
27
27
 
28
- expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
29
- new Set(["player1", "player2", "enemy1"]),
30
- )
31
- expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(
32
- new Set(["player1", "player2"]),
33
- )
28
+ expect(eventMap.getEntitiesForEvent("update")).toStrictEqual([
29
+ "player1",
30
+ "player2",
31
+ "enemy1",
32
+ ])
33
+ expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual([
34
+ "player1",
35
+ "player2",
36
+ ])
34
37
 
35
38
  // 'item' type has no events, so it shouldn't be in the map
36
- expect(eventMap.getEntitiesForEvent("item")).toStrictEqual(new Set())
39
+ expect(eventMap.getEntitiesForEvent("item")).toStrictEqual([])
37
40
  // 'ghost' type doesn't exist, so it should be ignored
38
- expect(eventMap.getEntitiesForEvent("ghost")).toStrictEqual(new Set())
41
+ expect(eventMap.getEntitiesForEvent("ghost")).toStrictEqual([])
39
42
  })
40
43
 
41
44
  test("addEntity should add an entity to the correct event sets", () => {
@@ -47,23 +50,21 @@ test("addEntity should add an entity to the correct event sets", () => {
47
50
  }
48
51
  const eventMap = new EventMap(types, {})
49
52
 
50
- eventMap.addEntity("player1", types.player)
53
+ eventMap.addEntity("player1", types.player, "player")
51
54
 
52
- expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
53
- new Set(["player1"]),
54
- )
55
- expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(
56
- new Set(["player1"]),
57
- )
55
+ expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player1"])
56
+ expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(["player1"])
58
57
 
59
58
  // Add another entity of the same type
60
- eventMap.addEntity("player2", types.player)
61
- expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
62
- new Set(["player1", "player2"]),
63
- )
64
- expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual(
65
- new Set(["player1", "player2"]),
66
- )
59
+ eventMap.addEntity("player2", types.player, "player")
60
+ expect(eventMap.getEntitiesForEvent("update")).toStrictEqual([
61
+ "player1",
62
+ "player2",
63
+ ])
64
+ expect(eventMap.getEntitiesForEvent("jump")).toStrictEqual([
65
+ "player1",
66
+ "player2",
67
+ ])
67
68
  })
68
69
 
69
70
  test("removeEntity should remove an entity from its event sets", () => {
@@ -79,17 +80,15 @@ test("removeEntity should remove an entity from its event sets", () => {
79
80
  }
80
81
  const eventMap = new EventMap(types, entities)
81
82
 
82
- eventMap.removeEntity("player1", types.player)
83
+ eventMap.removeEntity("player1", types.player, "player")
83
84
 
84
- expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(
85
- new Set(["player2"]),
86
- )
87
- expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(
88
- new Set(["player2"]),
89
- )
85
+ expect(eventMap.getEntitiesForEvent("update")).toStrictEqual(["player2"])
86
+ expect(eventMap.getEntitiesForEvent("fire")).toStrictEqual(["player2"])
90
87
 
91
88
  // Removing a non-existent entity should not throw an error
92
- expect(() => eventMap.removeEntity("player3", types.player)).not.toThrow()
89
+ expect(() =>
90
+ eventMap.removeEntity("player3", types.player, "player"),
91
+ ).not.toThrow()
93
92
  })
94
93
 
95
94
  test("getEntitiesForEvent should return the correct set of entities for an event", () => {
@@ -104,10 +103,57 @@ test("getEntitiesForEvent should return the correct set of entities for an event
104
103
  const eventMap = new EventMap(types, entities)
105
104
 
106
105
  const updateEntities = eventMap.getEntitiesForEvent("update")
107
- expect(updateEntities).toStrictEqual(new Set(["player1", "enemy1"]))
106
+ expect(updateEntities).toStrictEqual(["player1", "enemy1"])
108
107
 
109
108
  const fireEntities = eventMap.getEntitiesForEvent("fire")
110
- expect(fireEntities).toStrictEqual(new Set())
109
+ expect(fireEntities).toStrictEqual([])
110
+ })
111
+
112
+ test("getEntitiesForEvent should handle scoped events correctly", () => {
113
+ const types = {
114
+ form: { submit: () => {} },
115
+ button: { submit: () => {} },
116
+ }
117
+ const entities = {
118
+ loginForm: { type: "form" },
119
+ signupForm: { type: "form" },
120
+ submitButton: { type: "button" },
121
+ }
122
+ const eventMap = new EventMap(types, entities)
123
+
124
+ // Scoped to a type: 'form:submit' should only return form entities
125
+ expect(eventMap.getEntitiesForEvent("form:submit")).toStrictEqual([
126
+ "loginForm",
127
+ "signupForm",
128
+ ])
129
+
130
+ // Scoped to a specific entity by ID: '#loginForm:submit'
131
+ expect(eventMap.getEntitiesForEvent("#loginForm:submit")).toStrictEqual([
132
+ "loginForm",
133
+ ])
134
+
135
+ // Scoped to a specific entity by type and ID: 'form#loginForm:submit'
136
+ expect(eventMap.getEntitiesForEvent("form#loginForm:submit")).toStrictEqual([
137
+ "loginForm",
138
+ ])
139
+
140
+ // Scoped to a non-existent entity ID
141
+ expect(eventMap.getEntitiesForEvent("#nonExistent:submit")).toStrictEqual([])
142
+
143
+ // Scoped to an entity that exists but doesn't handle the event
144
+ expect(eventMap.getEntitiesForEvent("#loginForm:click")).toStrictEqual([])
145
+
146
+ // Scoped to an entity ID, but with the wrong type specified
147
+ expect(eventMap.getEntitiesForEvent("button#loginForm:submit")).toStrictEqual(
148
+ [],
149
+ )
150
+
151
+ // Broadcast event should return all entities with the handler
152
+ expect(eventMap.getEntitiesForEvent("submit")).toStrictEqual([
153
+ "loginForm",
154
+ "signupForm",
155
+ "submitButton",
156
+ ])
111
157
  })
112
158
 
113
159
  test("EventMap provides a significant performance benefit for event handling", async () => {
@@ -145,7 +191,7 @@ test("EventMap provides a significant performance benefit for event handling", a
145
191
 
146
192
  // Assertions to verify correctness
147
193
  expect(oldWayTime).toBeGreaterThan(newWayTime)
148
- expect(updateHandler).toHaveBeenCalledTimes(updaterIds.size)
194
+ expect(updateHandler).toHaveBeenCalledTimes(updaterIds.length)
149
195
  })
150
196
 
151
197
  // Helper function to create a large set of test entities
package/src/store.js CHANGED
@@ -2,7 +2,7 @@ import { create } from "mutative"
2
2
 
3
3
  import { createApi } from "./api.js"
4
4
  import { augmentEntities, augmentEntity } from "./entities.js"
5
- import { EventMap } from "./event-map.js"
5
+ import { EventMap, parseEvent } from "./event-map.js"
6
6
  import { applyMiddlewares } from "./middlewares.js"
7
7
  import { augmentType, augmentTypes } from "./types.js"
8
8
 
@@ -11,9 +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.
14
+ * @param {Array} [config.systems] - The initial systems configuration.
15
15
  * @param {Array} [config.middlewares] - The initial middlewares configuration.
16
- * @param {"eager" | "batched"} [config.mode] - The dispatch mode (defaults to "eager").
16
+ * @param {"auto" | "manual"} [config.updateMode] - The update mode (defaults to "auto").
17
17
  * @returns {Object} The store with methods to interact with state and events.
18
18
  */
19
19
  export function createStore({
@@ -21,13 +21,13 @@ export function createStore({
21
21
  entities: originalEntities,
22
22
  systems = [],
23
23
  middlewares = [],
24
- mode = "eager",
24
+ updateMode = "auto",
25
25
  }) {
26
26
  const listeners = new Set()
27
27
 
28
28
  const types = augmentTypes(originalTypes)
29
29
 
30
- let state, eventMap, incomingEvents
30
+ let state, eventMap, incomingEvents, isProcessing
31
31
  reset()
32
32
 
33
33
  const baseStore = {
@@ -36,6 +36,7 @@ export function createStore({
36
36
  notify,
37
37
  dispatch, // needed for compatibility with Redux
38
38
  getTypes,
39
+ getType,
39
40
  getState,
40
41
  setState,
41
42
  reset,
@@ -63,15 +64,21 @@ export function createStore({
63
64
 
64
65
  /**
65
66
  * Updates the state based on elapsed time and processes events.
66
- * @param {number} dt - The delta time since the last update in milliseconds.
67
67
  */
68
68
  function update() {
69
+ if (isProcessing) {
70
+ return []
71
+ }
72
+
73
+ isProcessing = true
69
74
  const processedEvents = []
70
75
 
71
76
  state = create(state, patcher, {
72
77
  enableAutoFreeze: state.game?.devMode,
73
78
  })
74
79
 
80
+ isProcessing = false
81
+
75
82
  listeners.forEach((onUpdate) => onUpdate())
76
83
 
77
84
  return processedEvents
@@ -81,18 +88,21 @@ export function createStore({
81
88
  const event = incomingEvents.shift()
82
89
  processedEvents.push(event)
83
90
 
91
+ // Handle special system events
84
92
  if (event.type === "morph") {
85
93
  const { id, type } = event.payload
86
94
 
87
95
  const entity = draft[id]
88
96
  const oldType = types[entity.type]
97
+ const oldTypeName = entity.type
89
98
 
90
- originalTypes[id] = type
91
- types[id] = augmentType(originalTypes[id])
92
- const newType = types[id]
99
+ originalTypes[entity.type] = type
100
+ types[entity.type] = augmentType(originalTypes[entity.type])
101
+ const newType = types[entity.type]
93
102
 
94
- eventMap.removeEntity(id, oldType)
95
- eventMap.addEntity(id, newType)
103
+ eventMap.removeEntity(id, oldType, oldTypeName)
104
+ eventMap.addEntity(id, newType, oldTypeName) // Use the original type name
105
+ continue
96
106
  }
97
107
 
98
108
  if (event.type === "add") {
@@ -100,30 +110,42 @@ export function createStore({
100
110
  draft[id] = augmentEntity(id, entity)
101
111
  const type = types[entity.type]
102
112
 
103
- eventMap.addEntity(id, type)
113
+ eventMap.addEntity(id, type, entity.type)
104
114
  incomingEvents.unshift({ type: "create", payload: id })
115
+ continue
105
116
  }
106
117
 
107
118
  if (event.type === "remove") {
108
119
  const id = event.payload
109
120
  const entity = draft[id]
110
121
  const type = types[entity.type]
122
+ const typeName = entity.type
111
123
  delete draft[id]
112
124
 
113
- eventMap.removeEntity(id, type)
125
+ eventMap.removeEntity(id, type, typeName)
114
126
  incomingEvents.unshift({ type: "destroy", payload: id })
127
+ continue
115
128
  }
116
129
 
130
+ // Parse the event to get handler name
131
+ const { event: handlerName } = parseEvent(event.type)
132
+
133
+ // Get entities that should handle this event (filtered by EventMap)
117
134
  const entityIds = eventMap.getEntitiesForEvent(event.type)
135
+
118
136
  for (const id of entityIds) {
119
137
  const entity = draft[id]
120
138
  const type = types[entity.type]
121
- const handle = type[event.type]
122
- handle(entity, event.payload, api)
139
+ const handle = type[handlerName]
140
+
141
+ if (handle) {
142
+ handle(entity, event.payload, api)
143
+ }
123
144
  }
124
145
 
146
+ // Systems process events by handler name (not scoped)
125
147
  systems.forEach((system) => {
126
- const handle = system[event.type]
148
+ const handle = system[handlerName]
127
149
  handle?.(draft, event.payload, api)
128
150
  })
129
151
  }
@@ -132,8 +154,13 @@ export function createStore({
132
154
 
133
155
  /**
134
156
  * Notifies the store of a new event.
135
- * @param {string} type - The event object type to notify.
136
- * @param {any} payload - The event object payload to notify.
157
+ * Supports scoped events:
158
+ * - 'submit' - broadcast to all entities with submit handler
159
+ * - 'form:submit' - only form entities
160
+ * - 'form[loginForm]:submit' - only loginForm entity
161
+ *
162
+ * @param {string} type - The event type to notify.
163
+ * @param {any} payload - The event payload.
137
164
  */
138
165
  function notify(type, payload) {
139
166
  // NOTE: it's important to invoke store.dispatch instead of dispatch, otherwise we cannot override it
@@ -148,7 +175,7 @@ export function createStore({
148
175
  */
149
176
  function dispatch(event) {
150
177
  incomingEvents.push(event)
151
- if (mode === "eager") {
178
+ if (updateMode === "auto") {
152
179
  update()
153
180
  }
154
181
  }
@@ -162,6 +189,15 @@ export function createStore({
162
189
  return types
163
190
  }
164
191
 
192
+ /**
193
+ * Retrieves an augmented type configuration given its name.
194
+ * @param {string} typeName - The type of the entity.
195
+ * @returns {Object} The augmented type configuration.
196
+ */
197
+ function getType(typeName) {
198
+ return types[typeName]
199
+ }
200
+
165
201
  /**
166
202
  * Retrieves the current state.
167
203
  * @returns {Object} The current state.
@@ -182,6 +218,7 @@ export function createStore({
182
218
  state = newEntities
183
219
  eventMap = new EventMap(types, nextState)
184
220
  incomingEvents = []
221
+ isProcessing = false
185
222
 
186
223
  const oldEntityIds = new Set(Object.keys(oldEntities))
187
224
  const newEntityIds = new Set(Object.keys(newEntities))
package/src/store.test.js CHANGED
@@ -86,7 +86,7 @@ test("it should send an event from an entity and process it in the same update c
86
86
  kitty1: { type: "kitty", position: "near" },
87
87
  },
88
88
 
89
- mode: "batched",
89
+ updateMode: "manual",
90
90
  }
91
91
  const afterState = {
92
92
  doggo1: { id: "doggo1", type: "doggo" },