@contember/bindx 0.1.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.
Files changed (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. package/src/utils/index.ts +2 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * EventEmitter manages event subscriptions and dispatching.
3
+ * Supports both scoped (entity/field-level) and global subscriptions.
4
+ */
5
+
6
+ import type {
7
+ AfterEvent,
8
+ AfterEventTypes,
9
+ BeforeEvent,
10
+ BeforeEventTypes,
11
+ EventListener,
12
+ EventTypeMap,
13
+ FieldScopedEvent,
14
+ Interceptor,
15
+ InterceptorResult,
16
+ Unsubscribe,
17
+ } from './types.js'
18
+
19
+ interface ScopeKey {
20
+ entityType: string
21
+ entityId: string
22
+ fieldName?: string
23
+ }
24
+
25
+ /**
26
+ * EventEmitter for the bindx event system.
27
+ *
28
+ * Provides:
29
+ * - Global event subscriptions (all events of a type)
30
+ * - Entity-scoped subscriptions (events for a specific entity)
31
+ * - Field-scoped subscriptions (events for a specific field/relation)
32
+ * - Interceptors for before-events (can cancel or modify)
33
+ */
34
+ export class EventEmitter {
35
+ // Global listeners by event type
36
+ private readonly globalListeners = new Map<string, Set<EventListener<AfterEvent>>>()
37
+
38
+ // Scoped listeners: "eventType:entityType:entityId[:fieldName]" -> Set<listener>
39
+ private readonly scopedListeners = new Map<string, Set<EventListener<AfterEvent>>>()
40
+
41
+ // Global interceptors by event type
42
+ private readonly globalInterceptors = new Map<string, Set<Interceptor<BeforeEvent>>>()
43
+
44
+ // Scoped interceptors
45
+ private readonly scopedInterceptors = new Map<string, Set<Interceptor<BeforeEvent>>>()
46
+
47
+ // ============================================================================
48
+ // Listener Subscription Methods
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Subscribe to an event globally (receives all events of that type).
53
+ */
54
+ on<T extends AfterEventTypes>(
55
+ eventType: T,
56
+ listener: EventListener<EventTypeMap[T] & AfterEvent>,
57
+ ): Unsubscribe {
58
+ const listeners = this.getOrCreateSet(this.globalListeners, eventType)
59
+ listeners.add(listener as EventListener<AfterEvent>)
60
+ return () => listeners.delete(listener as EventListener<AfterEvent>)
61
+ }
62
+
63
+ /**
64
+ * Subscribe to an event for a specific entity.
65
+ */
66
+ onEntity<T extends AfterEventTypes>(
67
+ eventType: T,
68
+ entityType: string,
69
+ entityId: string,
70
+ listener: EventListener<EventTypeMap[T] & AfterEvent>,
71
+ ): Unsubscribe {
72
+ const key = this.buildScopeKey(eventType, { entityType, entityId })
73
+ const listeners = this.getOrCreateSet(this.scopedListeners, key)
74
+ listeners.add(listener as EventListener<AfterEvent>)
75
+ return () => listeners.delete(listener as EventListener<AfterEvent>)
76
+ }
77
+
78
+ /**
79
+ * Subscribe to an event for a specific field or relation.
80
+ */
81
+ onField<T extends AfterEventTypes>(
82
+ eventType: T,
83
+ entityType: string,
84
+ entityId: string,
85
+ fieldName: string,
86
+ listener: EventListener<EventTypeMap[T] & AfterEvent>,
87
+ ): Unsubscribe {
88
+ const key = this.buildScopeKey(eventType, { entityType, entityId, fieldName })
89
+ const listeners = this.getOrCreateSet(this.scopedListeners, key)
90
+ listeners.add(listener as EventListener<AfterEvent>)
91
+ return () => listeners.delete(listener as EventListener<AfterEvent>)
92
+ }
93
+
94
+ // ============================================================================
95
+ // Interceptor Subscription Methods
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Add a global interceptor for a before event.
100
+ * Interceptors can cancel or modify the action.
101
+ */
102
+ intercept<T extends BeforeEventTypes>(
103
+ eventType: T,
104
+ interceptor: Interceptor<EventTypeMap[T] & BeforeEvent>,
105
+ ): Unsubscribe {
106
+ const interceptors = this.getOrCreateSet(this.globalInterceptors, eventType)
107
+ interceptors.add(interceptor as Interceptor<BeforeEvent>)
108
+ return () => interceptors.delete(interceptor as Interceptor<BeforeEvent>)
109
+ }
110
+
111
+ /**
112
+ * Add an entity-scoped interceptor.
113
+ */
114
+ interceptEntity<T extends BeforeEventTypes>(
115
+ eventType: T,
116
+ entityType: string,
117
+ entityId: string,
118
+ interceptor: Interceptor<EventTypeMap[T] & BeforeEvent>,
119
+ ): Unsubscribe {
120
+ const key = this.buildScopeKey(eventType, { entityType, entityId })
121
+ const interceptors = this.getOrCreateSet(this.scopedInterceptors, key)
122
+ interceptors.add(interceptor as Interceptor<BeforeEvent>)
123
+ return () => interceptors.delete(interceptor as Interceptor<BeforeEvent>)
124
+ }
125
+
126
+ /**
127
+ * Add a field-scoped interceptor.
128
+ */
129
+ interceptField<T extends BeforeEventTypes>(
130
+ eventType: T,
131
+ entityType: string,
132
+ entityId: string,
133
+ fieldName: string,
134
+ interceptor: Interceptor<EventTypeMap[T] & BeforeEvent>,
135
+ ): Unsubscribe {
136
+ const key = this.buildScopeKey(eventType, { entityType, entityId, fieldName })
137
+ const interceptors = this.getOrCreateSet(this.scopedInterceptors, key)
138
+ interceptors.add(interceptor as Interceptor<BeforeEvent>)
139
+ return () => interceptors.delete(interceptor as Interceptor<BeforeEvent>)
140
+ }
141
+
142
+ // ============================================================================
143
+ // Dispatch Methods
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Runs interceptors for a before event.
148
+ * Returns the (possibly modified) event or null if cancelled.
149
+ */
150
+ async runInterceptors<T extends BeforeEvent>(event: T): Promise<T | null> {
151
+ let currentEvent: T = event
152
+
153
+ // Get field name if this is a field-scoped event
154
+ const fieldName = this.getFieldNameFromEvent(event)
155
+
156
+ // Run scoped interceptors first (more specific takes precedence)
157
+ // Field-level interceptors
158
+ if (fieldName) {
159
+ const fieldKey = this.buildScopeKey(event.type, {
160
+ entityType: event.entityType,
161
+ entityId: event.entityId,
162
+ fieldName,
163
+ })
164
+ const result = await this.runInterceptorSet(
165
+ this.scopedInterceptors.get(fieldKey),
166
+ currentEvent,
167
+ )
168
+ if (result === null) return null
169
+ // Type assertion safe: interceptors return same event type
170
+ currentEvent = result as T
171
+ }
172
+
173
+ // Entity-level interceptors
174
+ const entityKey = this.buildScopeKey(event.type, {
175
+ entityType: event.entityType,
176
+ entityId: event.entityId,
177
+ })
178
+ const entityResult = await this.runInterceptorSet(
179
+ this.scopedInterceptors.get(entityKey),
180
+ currentEvent,
181
+ )
182
+ if (entityResult === null) return null
183
+ // Type assertion safe: interceptors return same event type
184
+ currentEvent = entityResult as T
185
+
186
+ // Global interceptors
187
+ const globalResult = await this.runInterceptorSet(
188
+ this.globalInterceptors.get(event.type),
189
+ currentEvent,
190
+ )
191
+ if (globalResult === null) return null
192
+
193
+ // Type assertion safe: interceptors return same event type
194
+ return globalResult as T
195
+ }
196
+
197
+ /**
198
+ * Emits an after event to all listeners.
199
+ */
200
+ emit<T extends AfterEvent>(event: T): void {
201
+ // Get field name if this is a field-scoped event
202
+ const fieldName = this.getFieldNameFromEvent(event)
203
+
204
+ // Emit to scoped listeners first (most specific to least specific)
205
+ // Field-level listeners
206
+ if (fieldName) {
207
+ const fieldKey = this.buildScopeKey(event.type, {
208
+ entityType: event.entityType,
209
+ entityId: event.entityId,
210
+ fieldName,
211
+ })
212
+ this.notifyListeners(this.scopedListeners.get(fieldKey), event)
213
+ }
214
+
215
+ // Entity-level listeners
216
+ const entityKey = this.buildScopeKey(event.type, {
217
+ entityType: event.entityType,
218
+ entityId: event.entityId,
219
+ })
220
+ this.notifyListeners(this.scopedListeners.get(entityKey), event)
221
+
222
+ // Global listeners
223
+ this.notifyListeners(this.globalListeners.get(event.type), event)
224
+ }
225
+
226
+ // ============================================================================
227
+ // Helper Methods
228
+ // ============================================================================
229
+
230
+ /**
231
+ * Builds a scope key for listener/interceptor lookup.
232
+ */
233
+ private buildScopeKey(eventType: string, scope: ScopeKey): string {
234
+ const parts = [eventType, scope.entityType, scope.entityId]
235
+ if (scope.fieldName) {
236
+ parts.push(scope.fieldName)
237
+ }
238
+ return parts.join(':')
239
+ }
240
+
241
+ /**
242
+ * Gets or creates a Set in a Map.
243
+ */
244
+ private getOrCreateSet<T>(map: Map<string, Set<T>>, key: string): Set<T> {
245
+ let set = map.get(key)
246
+ if (!set) {
247
+ set = new Set()
248
+ map.set(key, set)
249
+ }
250
+ return set
251
+ }
252
+
253
+ /**
254
+ * Extracts fieldName from a field-scoped event.
255
+ */
256
+ private getFieldNameFromEvent(event: BeforeEvent | AfterEvent): string | undefined {
257
+ if ('fieldName' in event && typeof event.fieldName === 'string') {
258
+ return event.fieldName
259
+ }
260
+ return undefined
261
+ }
262
+
263
+ /**
264
+ * Notifies all listeners in a set.
265
+ */
266
+ private notifyListeners<T extends AfterEvent>(
267
+ listeners: Set<EventListener<T>> | undefined,
268
+ event: T,
269
+ ): void {
270
+ if (!listeners) return
271
+ for (const listener of listeners) {
272
+ try {
273
+ listener(event)
274
+ } catch (error) {
275
+ console.error('[Bindx EventEmitter] Listener error:', error)
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Runs interceptors for a before event synchronously.
282
+ * Returns the (possibly modified) event or null if cancelled.
283
+ * If an interceptor returns a Promise, logs a warning and skips it.
284
+ */
285
+ runInterceptorsSync<T extends BeforeEvent>(event: T): T | null {
286
+ let currentEvent: T = event
287
+
288
+ // Get field name if this is a field-scoped event
289
+ const fieldName = this.getFieldNameFromEvent(event)
290
+
291
+ // Run scoped interceptors first (more specific takes precedence)
292
+ // Field-level interceptors
293
+ if (fieldName) {
294
+ const fieldKey = this.buildScopeKey(event.type, {
295
+ entityType: event.entityType,
296
+ entityId: event.entityId,
297
+ fieldName,
298
+ })
299
+ const result = this.runInterceptorSetSync(
300
+ this.scopedInterceptors.get(fieldKey),
301
+ currentEvent,
302
+ )
303
+ if (result === null) return null
304
+ currentEvent = result as T
305
+ }
306
+
307
+ // Entity-level interceptors
308
+ const entityKey = this.buildScopeKey(event.type, {
309
+ entityType: event.entityType,
310
+ entityId: event.entityId,
311
+ })
312
+ const entityResult = this.runInterceptorSetSync(
313
+ this.scopedInterceptors.get(entityKey),
314
+ currentEvent,
315
+ )
316
+ if (entityResult === null) return null
317
+ currentEvent = entityResult as T
318
+
319
+ // Global interceptors
320
+ const globalResult = this.runInterceptorSetSync(
321
+ this.globalInterceptors.get(event.type),
322
+ currentEvent,
323
+ )
324
+ if (globalResult === null) return null
325
+
326
+ return globalResult as T
327
+ }
328
+
329
+ /**
330
+ * Runs all interceptors in a set.
331
+ * Returns the (possibly modified) event or null if cancelled.
332
+ */
333
+ private async runInterceptorSet<T extends BeforeEvent>(
334
+ interceptors: Set<Interceptor<T>> | undefined,
335
+ event: T,
336
+ ): Promise<T | null> {
337
+ if (!interceptors || interceptors.size === 0) return event
338
+
339
+ let currentEvent = event
340
+ for (const interceptor of interceptors) {
341
+ try {
342
+ const result = await interceptor(currentEvent)
343
+
344
+ // No explicit return or undefined means continue
345
+ if (result === undefined || result === null) {
346
+ continue
347
+ }
348
+
349
+ const typedResult = result as InterceptorResult<T>
350
+
351
+ if (typedResult.action === 'cancel') {
352
+ return null
353
+ }
354
+
355
+ if (typedResult.action === 'modify' && 'event' in typedResult) {
356
+ currentEvent = typedResult.event
357
+ }
358
+ // 'continue' - just proceed with current event
359
+ } catch (error) {
360
+ console.error('[Bindx EventEmitter] Interceptor error:', error)
361
+ // On error, cancel the action for safety
362
+ return null
363
+ }
364
+ }
365
+
366
+ return currentEvent
367
+ }
368
+
369
+ /**
370
+ * Runs all interceptors in a set synchronously.
371
+ * If an interceptor returns a Promise, logs a warning and skips it.
372
+ */
373
+ private runInterceptorSetSync<T extends BeforeEvent>(
374
+ interceptors: Set<Interceptor<T>> | undefined,
375
+ event: T,
376
+ ): T | null {
377
+ if (!interceptors || interceptors.size === 0) return event
378
+
379
+ let currentEvent = event
380
+ for (const interceptor of interceptors) {
381
+ try {
382
+ const result = interceptor(currentEvent)
383
+
384
+ // If the interceptor returned a Promise, skip it with a warning
385
+ if (result !== null && result !== undefined && typeof result === 'object' && 'then' in result) {
386
+ console.warn(
387
+ '[Bindx EventEmitter] Async interceptor used with synchronous dispatch. ' +
388
+ 'Use dispatchAsync() for async interceptors.',
389
+ )
390
+ continue
391
+ }
392
+
393
+ // No explicit return or undefined means continue
394
+ if (result === undefined || result === null) {
395
+ continue
396
+ }
397
+
398
+ const typedResult = result as InterceptorResult<T>
399
+
400
+ if (typedResult.action === 'cancel') {
401
+ return null
402
+ }
403
+
404
+ if (typedResult.action === 'modify' && 'event' in typedResult) {
405
+ currentEvent = typedResult.event
406
+ }
407
+ // 'continue' - just proceed with current event
408
+ } catch (error) {
409
+ console.error('[Bindx EventEmitter] Interceptor error:', error)
410
+ // On error, cancel the action for safety
411
+ return null
412
+ }
413
+ }
414
+
415
+ return currentEvent
416
+ }
417
+
418
+ /**
419
+ * Clears all listeners and interceptors.
420
+ * Useful for testing or cleanup.
421
+ */
422
+ clear(): void {
423
+ this.globalListeners.clear()
424
+ this.scopedListeners.clear()
425
+ this.globalInterceptors.clear()
426
+ this.scopedInterceptors.clear()
427
+ }
428
+
429
+ /**
430
+ * Returns the number of listeners for a given event type.
431
+ * Useful for debugging.
432
+ */
433
+ listenerCount(eventType: string): number {
434
+ let count = 0
435
+ const global = this.globalListeners.get(eventType)
436
+ if (global) count += global.size
437
+
438
+ // Count scoped listeners
439
+ for (const [key, listeners] of this.scopedListeners) {
440
+ if (key.startsWith(eventType + ':')) {
441
+ count += listeners.size
442
+ }
443
+ }
444
+
445
+ return count
446
+ }
447
+ }