@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.
- package/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- 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
|
+
}
|