bunsane 0.1.0 → 0.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/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +119 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +159 -12
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +453 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +65 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +1 -1
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/examples/hooks/README.md +228 -0
- package/examples/hooks/audit-logger.ts +495 -0
- package/gql/Generator.ts +56 -34
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +55 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +5 -4
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +205 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- package/validate-docs.sh +90 -0
- package/core/Events.ts +0 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
import type { Entity } from "./Entity";
|
|
2
|
+
import type { BaseComponent } from "./Components";
|
|
3
|
+
import ArcheType from "./ArcheType";
|
|
4
|
+
import {
|
|
5
|
+
EntityLifecycleEvent,
|
|
6
|
+
EntityCreatedEvent,
|
|
7
|
+
EntityUpdatedEvent,
|
|
8
|
+
EntityDeletedEvent,
|
|
9
|
+
ComponentLifecycleEvent,
|
|
10
|
+
ComponentAddedEvent,
|
|
11
|
+
ComponentUpdatedEvent,
|
|
12
|
+
ComponentRemovedEvent,
|
|
13
|
+
type EntityEvent,
|
|
14
|
+
type ComponentEvent,
|
|
15
|
+
type LifecycleEvent
|
|
16
|
+
} from "./events/EntityLifecycleEvents";
|
|
17
|
+
import { logger as MainLogger } from "./Logger";
|
|
18
|
+
import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
|
|
19
|
+
|
|
20
|
+
const logger = MainLogger.child({ scope: "EntityHookManager" });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook callback function signature for entity events
|
|
24
|
+
*/
|
|
25
|
+
export type EntityHookCallback<T extends EntityEvent = EntityEvent> = (event: T) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook callback function signature for component events
|
|
29
|
+
*/
|
|
30
|
+
export type ComponentHookCallback<T extends ComponentEvent = ComponentEvent> = (event: T) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook callback function signature for any lifecycle event
|
|
34
|
+
*/
|
|
35
|
+
export type LifecycleHookCallback = (event: LifecycleEvent) => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Component targeting configuration for hooks
|
|
39
|
+
*/
|
|
40
|
+
export interface ComponentTargetConfig {
|
|
41
|
+
/** Component types that must be present on the entity for the hook to execute */
|
|
42
|
+
includeComponents?: (new () => BaseComponent)[];
|
|
43
|
+
/** Component types that must NOT be present on the entity for the hook to execute */
|
|
44
|
+
excludeComponents?: (new () => BaseComponent)[];
|
|
45
|
+
/** Whether to require ALL included components (AND) or ANY included component (OR) */
|
|
46
|
+
requireAllIncluded?: boolean;
|
|
47
|
+
/** Whether to require ALL excluded components to be absent (AND) or ANY excluded component to be absent (OR) */
|
|
48
|
+
requireAllExcluded?: boolean;
|
|
49
|
+
/** Archetype to match - entity must have exactly these component types */
|
|
50
|
+
archetype?: ArcheType;
|
|
51
|
+
/** Archetypes to match - entity must match ANY of these archetypes */
|
|
52
|
+
archetypes?: ArcheType[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook registration options
|
|
57
|
+
*/
|
|
58
|
+
export interface HookOptions {
|
|
59
|
+
/** Priority for hook execution order (higher numbers execute first) */
|
|
60
|
+
priority?: number;
|
|
61
|
+
/** Optional name for the hook for debugging */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Whether the hook should be executed asynchronously */
|
|
64
|
+
async?: boolean;
|
|
65
|
+
/** Filter function to conditionally execute the hook */
|
|
66
|
+
filter?: (event: LifecycleEvent) => boolean;
|
|
67
|
+
/** Maximum execution time in milliseconds (for timeout handling) */
|
|
68
|
+
timeout?: number;
|
|
69
|
+
/** Component targeting configuration for fine-grained hook execution */
|
|
70
|
+
componentTarget?: ComponentTargetConfig;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Registered hook information
|
|
75
|
+
*/
|
|
76
|
+
interface RegisteredHook {
|
|
77
|
+
callback: LifecycleHookCallback;
|
|
78
|
+
options: HookOptions;
|
|
79
|
+
id: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hook execution metrics
|
|
84
|
+
*/
|
|
85
|
+
interface HookMetrics {
|
|
86
|
+
totalExecutions: number;
|
|
87
|
+
totalExecutionTime: number;
|
|
88
|
+
averageExecutionTime: number;
|
|
89
|
+
errorCount: number;
|
|
90
|
+
lastExecutionTime: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* EntityHookManager - Singleton for managing entity lifecycle hooks
|
|
95
|
+
* Provides registration and execution of hooks for entity and component lifecycle events
|
|
96
|
+
*/
|
|
97
|
+
class EntityHookManager {
|
|
98
|
+
private static _instance: EntityHookManager;
|
|
99
|
+
private hooks: Map<string, RegisteredHook[]> = new Map();
|
|
100
|
+
private hookCounter: number = 0;
|
|
101
|
+
private metrics: Map<string, HookMetrics> = new Map();
|
|
102
|
+
private globalMetrics: HookMetrics = {
|
|
103
|
+
totalExecutions: 0,
|
|
104
|
+
totalExecutionTime: 0,
|
|
105
|
+
averageExecutionTime: 0,
|
|
106
|
+
errorCount: 0,
|
|
107
|
+
lastExecutionTime: 0
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
private constructor() {
|
|
111
|
+
logger.trace("EntityHookManager initialized");
|
|
112
|
+
this.initializeLifecycleIntegration();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Initialize integration with ApplicationLifecycle
|
|
117
|
+
*/
|
|
118
|
+
private initializeLifecycleIntegration(): void {
|
|
119
|
+
// Wait for components to be ready before allowing hook registration
|
|
120
|
+
ApplicationLifecycle.addPhaseListener((event) => {
|
|
121
|
+
const phase = event.detail;
|
|
122
|
+
switch (phase) {
|
|
123
|
+
case ApplicationPhase.COMPONENTS_READY:
|
|
124
|
+
logger.info("EntityHookManager ready for hook registration");
|
|
125
|
+
break;
|
|
126
|
+
case ApplicationPhase.APPLICATION_READY:
|
|
127
|
+
logger.info("EntityHookManager fully operational");
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Wait for the hook system to be ready for registration
|
|
135
|
+
*/
|
|
136
|
+
public async waitForReady(): Promise<void> {
|
|
137
|
+
await ApplicationLifecycle.waitForPhase(ApplicationPhase.COMPONENTS_READY);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if the hook system is ready for registration
|
|
142
|
+
*/
|
|
143
|
+
public isReady(): boolean {
|
|
144
|
+
return ApplicationLifecycle.getCurrentPhase() >= ApplicationPhase.COMPONENTS_READY;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Register a hook for entity lifecycle events
|
|
149
|
+
* @param eventType The event type to hook into
|
|
150
|
+
* @param callback The callback function to execute
|
|
151
|
+
* @param options Hook registration options
|
|
152
|
+
* @returns Hook ID for later removal
|
|
153
|
+
*/
|
|
154
|
+
public registerEntityHook<T extends EntityEvent>(
|
|
155
|
+
eventType: T['eventType'],
|
|
156
|
+
callback: EntityHookCallback<T>,
|
|
157
|
+
options: HookOptions = {}
|
|
158
|
+
): string {
|
|
159
|
+
const hookId = this.generateHookId();
|
|
160
|
+
const hook: RegisteredHook = {
|
|
161
|
+
callback: callback as LifecycleHookCallback,
|
|
162
|
+
options: { priority: 0, ...options },
|
|
163
|
+
id: hookId
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (!this.hooks.has(eventType)) {
|
|
167
|
+
this.hooks.set(eventType, []);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.hooks.get(eventType)!.push(hook);
|
|
171
|
+
this.sortHooksByPriority(eventType);
|
|
172
|
+
|
|
173
|
+
logger.trace(`Registered entity hook ${hookId} for event type: ${eventType}`);
|
|
174
|
+
return hookId;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a hook for component lifecycle events
|
|
179
|
+
* @param eventType The event type to hook into
|
|
180
|
+
* @param callback The callback function to execute
|
|
181
|
+
* @param options Hook registration options
|
|
182
|
+
* @returns Hook ID for later removal
|
|
183
|
+
*/
|
|
184
|
+
public registerComponentHook<T extends ComponentEvent>(
|
|
185
|
+
eventType: T['eventType'],
|
|
186
|
+
callback: ComponentHookCallback<T>,
|
|
187
|
+
options: HookOptions = {}
|
|
188
|
+
): string {
|
|
189
|
+
const hookId = this.generateHookId();
|
|
190
|
+
const hook: RegisteredHook = {
|
|
191
|
+
callback: callback as LifecycleHookCallback,
|
|
192
|
+
options: { priority: 0, ...options },
|
|
193
|
+
id: hookId
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (!this.hooks.has(eventType)) {
|
|
197
|
+
this.hooks.set(eventType, []);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.hooks.get(eventType)!.push(hook);
|
|
201
|
+
this.sortHooksByPriority(eventType);
|
|
202
|
+
|
|
203
|
+
logger.trace(`Registered component hook ${hookId} for event type: ${eventType}`);
|
|
204
|
+
return hookId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Register a hook for all lifecycle events
|
|
209
|
+
* @param callback The callback function to execute
|
|
210
|
+
* @param options Hook registration options
|
|
211
|
+
* @returns Hook ID for later removal
|
|
212
|
+
*/
|
|
213
|
+
public registerLifecycleHook(
|
|
214
|
+
callback: LifecycleHookCallback,
|
|
215
|
+
options: HookOptions = {}
|
|
216
|
+
): string {
|
|
217
|
+
const hookId = this.generateHookId();
|
|
218
|
+
const hook: RegisteredHook = {
|
|
219
|
+
callback,
|
|
220
|
+
options: { priority: 0, ...options },
|
|
221
|
+
id: hookId
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Register for all event types
|
|
225
|
+
const allEventTypes = [
|
|
226
|
+
"entity.created", "entity.updated", "entity.deleted",
|
|
227
|
+
"component.added", "component.updated", "component.removed"
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
for (const eventType of allEventTypes) {
|
|
231
|
+
if (!this.hooks.has(eventType)) {
|
|
232
|
+
this.hooks.set(eventType, []);
|
|
233
|
+
}
|
|
234
|
+
this.hooks.get(eventType)!.push({ ...hook }); // Clone hook for each event type
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
logger.trace(`Registered lifecycle hook ${hookId} for all event types`);
|
|
238
|
+
return hookId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Remove a hook by its ID
|
|
243
|
+
* @param hookId The hook ID to remove
|
|
244
|
+
* @returns True if hook was removed, false if not found
|
|
245
|
+
*/
|
|
246
|
+
public removeHook(hookId: string): boolean {
|
|
247
|
+
let removed = false;
|
|
248
|
+
|
|
249
|
+
for (const [eventType, hooks] of this.hooks.entries()) {
|
|
250
|
+
const initialLength = hooks.length;
|
|
251
|
+
this.hooks.set(eventType, hooks.filter(hook => hook.id !== hookId));
|
|
252
|
+
|
|
253
|
+
if (this.hooks.get(eventType)!.length < initialLength) {
|
|
254
|
+
removed = true;
|
|
255
|
+
logger.trace(`Removed hook ${hookId} from event type: ${eventType}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return removed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Execute hooks for a specific event
|
|
264
|
+
* @param event The lifecycle event to process
|
|
265
|
+
*/
|
|
266
|
+
public async executeHooks(event: LifecycleEvent): Promise<void> {
|
|
267
|
+
const eventType = event.getEventType();
|
|
268
|
+
const hooks = this.hooks.get(eventType) || [];
|
|
269
|
+
const startTime = performance.now();
|
|
270
|
+
let hadErrors = false;
|
|
271
|
+
|
|
272
|
+
if (hooks.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
logger.trace(`Executing ${hooks.length} hooks for event: ${eventType}`);
|
|
277
|
+
|
|
278
|
+
// Separate sync and async hooks
|
|
279
|
+
const syncHooks = hooks.filter(hook => !hook.options.async);
|
|
280
|
+
const asyncHooks = hooks.filter(hook => hook.options.async);
|
|
281
|
+
|
|
282
|
+
// Execute sync hooks immediately
|
|
283
|
+
for (const hook of syncHooks) {
|
|
284
|
+
// Check component targeting first
|
|
285
|
+
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check filter condition
|
|
290
|
+
if (hook.options.filter && !hook.options.filter(event)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
296
|
+
// Execute with timeout
|
|
297
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
298
|
+
setTimeout(() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)), hook.options.timeout);
|
|
299
|
+
});
|
|
300
|
+
await Promise.race([hook.callback(event), timeoutPromise]);
|
|
301
|
+
} else {
|
|
302
|
+
// Execute normally
|
|
303
|
+
hook.callback(event);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
|
|
307
|
+
hadErrors = true;
|
|
308
|
+
// Continue executing other hooks even if one fails
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Execute async hooks in parallel
|
|
313
|
+
if (asyncHooks.length > 0) {
|
|
314
|
+
const asyncPromises = asyncHooks.map(async (hook) => {
|
|
315
|
+
// Check component targeting first
|
|
316
|
+
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check filter condition
|
|
321
|
+
if (hook.options.filter && !hook.options.filter(event)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
327
|
+
// Execute with timeout
|
|
328
|
+
const hookPromise = hook.callback(event);
|
|
329
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
330
|
+
setTimeout(() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)), hook.options.timeout);
|
|
331
|
+
});
|
|
332
|
+
await Promise.race([hookPromise, timeoutPromise]);
|
|
333
|
+
} else {
|
|
334
|
+
// Execute normally
|
|
335
|
+
await hook.callback(event);
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
|
|
339
|
+
hadErrors = true;
|
|
340
|
+
// Continue executing other hooks even if one fails
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await Promise.allSettled(asyncPromises);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Record performance metrics
|
|
348
|
+
const executionTime = performance.now() - startTime;
|
|
349
|
+
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
350
|
+
} /**
|
|
351
|
+
* Execute hooks for multiple events in batch
|
|
352
|
+
* @param events Array of lifecycle events to process
|
|
353
|
+
*/
|
|
354
|
+
public async executeHooksBatch(events: LifecycleEvent[]): Promise<void> {
|
|
355
|
+
if (events.length === 0) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
logger.trace(`Executing hooks for ${events.length} events in batch`);
|
|
360
|
+
|
|
361
|
+
// Group events by type for efficient processing
|
|
362
|
+
const eventsByType = new Map<string, LifecycleEvent[]>();
|
|
363
|
+
for (const event of events) {
|
|
364
|
+
const eventType = event.getEventType();
|
|
365
|
+
if (!eventsByType.has(eventType)) {
|
|
366
|
+
eventsByType.set(eventType, []);
|
|
367
|
+
}
|
|
368
|
+
eventsByType.get(eventType)!.push(event);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Process each event type
|
|
372
|
+
const promises: Promise<void>[] = [];
|
|
373
|
+
for (const [eventType, typeEvents] of eventsByType.entries()) {
|
|
374
|
+
promises.push(this.executeHooksForType(eventType, typeEvents));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await Promise.allSettled(promises);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Execute hooks for a specific event type with multiple events
|
|
382
|
+
* @param eventType The event type
|
|
383
|
+
* @param events Array of events of the same type
|
|
384
|
+
*/
|
|
385
|
+
private async executeHooksForType(eventType: string, events: LifecycleEvent[]): Promise<void> {
|
|
386
|
+
const hooks = this.hooks.get(eventType) || [];
|
|
387
|
+
|
|
388
|
+
if (hooks.length === 0 || events.length === 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
logger.trace(`Executing ${hooks.length} hooks for ${events.length} ${eventType} events`);
|
|
393
|
+
|
|
394
|
+
// Pre-filter hooks by component targeting to avoid repeated checks
|
|
395
|
+
const preFilteredHooks = this.preFilterHooksByComponentTargeting(hooks, events);
|
|
396
|
+
|
|
397
|
+
if (preFilteredHooks.length === 0) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Separate sync and async hooks
|
|
402
|
+
const syncHooks = preFilteredHooks.filter(hook => !hook.options.async);
|
|
403
|
+
const asyncHooks = preFilteredHooks.filter(hook => hook.options.async);
|
|
404
|
+
|
|
405
|
+
// Execute sync hooks for all events with batch optimization
|
|
406
|
+
if (syncHooks.length > 0) {
|
|
407
|
+
await this.executeSyncHooksBatch(syncHooks, events, eventType);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Execute async hooks in parallel for all events with batch optimization
|
|
411
|
+
if (asyncHooks.length > 0) {
|
|
412
|
+
await this.executeAsyncHooksBatch(asyncHooks, events, eventType);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Pre-filter hooks based on component targeting to optimize batch processing
|
|
418
|
+
* @param hooks Array of hooks to filter
|
|
419
|
+
* @param events Array of events to check against
|
|
420
|
+
* @returns Array of hooks that could potentially match any of the events
|
|
421
|
+
*/
|
|
422
|
+
private preFilterHooksByComponentTargeting(hooks: RegisteredHook[], events: LifecycleEvent[]): RegisteredHook[] {
|
|
423
|
+
// If no hooks have component targeting, return all hooks (preserving order)
|
|
424
|
+
const hasComponentTargeting = hooks.some(hook => hook.options.componentTarget);
|
|
425
|
+
if (!hasComponentTargeting) {
|
|
426
|
+
return [...hooks]; // Return a copy to avoid modifying the original
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// For hooks with component targeting, check if they could match any event
|
|
430
|
+
// This is a broad pre-filter to avoid checking every hook against every event
|
|
431
|
+
const filteredHooks = hooks.filter(hook => {
|
|
432
|
+
if (!hook.options.componentTarget) {
|
|
433
|
+
return true; // No targeting means it matches all
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check if this hook could potentially match any of the events
|
|
437
|
+
return events.some(event => this.matchesComponentTarget(event, hook.options.componentTarget));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Return filtered hooks in their original order (priority should already be sorted)
|
|
441
|
+
return filteredHooks;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Execute sync hooks for multiple events with batch optimizations
|
|
446
|
+
* @param syncHooks Array of synchronous hooks
|
|
447
|
+
* @param events Array of events
|
|
448
|
+
* @param eventType The event type
|
|
449
|
+
*/
|
|
450
|
+
private async executeSyncHooksBatch(syncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
|
|
451
|
+
const startTime = performance.now();
|
|
452
|
+
let hadErrors = false;
|
|
453
|
+
|
|
454
|
+
// Execute hooks in priority order across all events to maintain deterministic execution
|
|
455
|
+
for (const hook of syncHooks) {
|
|
456
|
+
// Process all events for this hook
|
|
457
|
+
for (const event of events) {
|
|
458
|
+
// Double-check component targeting (pre-filter may have false positives)
|
|
459
|
+
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check filter condition
|
|
464
|
+
if (hook.options.filter && !hook.options.filter(event)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
470
|
+
// Execute with timeout
|
|
471
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
472
|
+
setTimeout(() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)), hook.options.timeout);
|
|
473
|
+
});
|
|
474
|
+
await Promise.race([hook.callback(event), timeoutPromise]);
|
|
475
|
+
} else {
|
|
476
|
+
// Execute normally
|
|
477
|
+
hook.callback(event);
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
|
|
481
|
+
hadErrors = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Record performance metrics
|
|
487
|
+
const executionTime = performance.now() - startTime;
|
|
488
|
+
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Execute async hooks for multiple events with batch optimizations
|
|
493
|
+
* @param asyncHooks Array of asynchronous hooks
|
|
494
|
+
* @param events Array of events
|
|
495
|
+
* @param eventType The event type
|
|
496
|
+
*/
|
|
497
|
+
private async executeAsyncHooksBatch(asyncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
|
|
498
|
+
const startTime = performance.now();
|
|
499
|
+
let hadErrors = false;
|
|
500
|
+
|
|
501
|
+
// Collect all async hook executions
|
|
502
|
+
const asyncPromises: Promise<void>[] = [];
|
|
503
|
+
|
|
504
|
+
// Use a more efficient batching strategy for async hooks
|
|
505
|
+
for (const event of events) {
|
|
506
|
+
for (const hook of asyncHooks) {
|
|
507
|
+
// Double-check component targeting
|
|
508
|
+
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check filter condition
|
|
513
|
+
if (hook.options.filter && !hook.options.filter(event)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
asyncPromises.push(
|
|
518
|
+
(async () => {
|
|
519
|
+
try {
|
|
520
|
+
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
521
|
+
// Execute with timeout
|
|
522
|
+
const hookPromise = hook.callback(event);
|
|
523
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
524
|
+
setTimeout(() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)), hook.options.timeout);
|
|
525
|
+
});
|
|
526
|
+
await Promise.race([hookPromise, timeoutPromise]);
|
|
527
|
+
} else {
|
|
528
|
+
// Execute normally
|
|
529
|
+
await hook.callback(event);
|
|
530
|
+
}
|
|
531
|
+
} catch (error) {
|
|
532
|
+
logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
|
|
533
|
+
hadErrors = true;
|
|
534
|
+
}
|
|
535
|
+
})()
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Execute all async hooks in parallel with controlled concurrency
|
|
541
|
+
if (asyncPromises.length > 0) {
|
|
542
|
+
await Promise.allSettled(asyncPromises);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Record performance metrics
|
|
546
|
+
const executionTime = performance.now() - startTime;
|
|
547
|
+
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get the number of registered hooks for an event type
|
|
552
|
+
* @param eventType The event type to check
|
|
553
|
+
* @returns Number of registered hooks
|
|
554
|
+
*/
|
|
555
|
+
public getHookCount(eventType?: string): number {
|
|
556
|
+
if (eventType) {
|
|
557
|
+
return this.hooks.get(eventType)?.length || 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let total = 0;
|
|
561
|
+
for (const hooks of this.hooks.values()) {
|
|
562
|
+
total += hooks.length;
|
|
563
|
+
}
|
|
564
|
+
return total;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get performance metrics for hook execution
|
|
569
|
+
* @param eventType Optional event type to get specific metrics
|
|
570
|
+
* @returns Hook execution metrics
|
|
571
|
+
*/
|
|
572
|
+
public getMetrics(eventType?: string): HookMetrics {
|
|
573
|
+
if (eventType) {
|
|
574
|
+
return this.metrics.get(eventType) || {
|
|
575
|
+
totalExecutions: 0,
|
|
576
|
+
totalExecutionTime: 0,
|
|
577
|
+
averageExecutionTime: 0,
|
|
578
|
+
errorCount: 0,
|
|
579
|
+
lastExecutionTime: 0
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return { ...this.globalMetrics };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Reset performance metrics
|
|
587
|
+
* @param eventType Optional event type to reset specific metrics
|
|
588
|
+
*/
|
|
589
|
+
public resetMetrics(eventType?: string): void {
|
|
590
|
+
if (eventType) {
|
|
591
|
+
this.metrics.delete(eventType);
|
|
592
|
+
} else {
|
|
593
|
+
this.metrics.clear();
|
|
594
|
+
this.globalMetrics = {
|
|
595
|
+
totalExecutions: 0,
|
|
596
|
+
totalExecutionTime: 0,
|
|
597
|
+
averageExecutionTime: 0,
|
|
598
|
+
errorCount: 0,
|
|
599
|
+
lastExecutionTime: 0
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
logger.trace(`Reset metrics${eventType ? ` for ${eventType}` : ''}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Clear all hooks (useful for testing)
|
|
607
|
+
*/
|
|
608
|
+
public clearAllHooks(): void {
|
|
609
|
+
this.hooks.clear();
|
|
610
|
+
this.hookCounter = 0;
|
|
611
|
+
logger.trace("Cleared all hooks");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Record hook execution metrics
|
|
616
|
+
* @param eventType The event type
|
|
617
|
+
* @param executionTime Time taken to execute hooks
|
|
618
|
+
* @param hadErrors Whether any hooks had errors
|
|
619
|
+
*/
|
|
620
|
+
private recordMetrics(eventType: string, executionTime: number, hadErrors: boolean): void {
|
|
621
|
+
// Update event-specific metrics
|
|
622
|
+
let eventMetrics = this.metrics.get(eventType);
|
|
623
|
+
if (!eventMetrics) {
|
|
624
|
+
eventMetrics = {
|
|
625
|
+
totalExecutions: 0,
|
|
626
|
+
totalExecutionTime: 0,
|
|
627
|
+
averageExecutionTime: 0,
|
|
628
|
+
errorCount: 0,
|
|
629
|
+
lastExecutionTime: 0
|
|
630
|
+
};
|
|
631
|
+
this.metrics.set(eventType, eventMetrics);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
eventMetrics.totalExecutions++;
|
|
635
|
+
eventMetrics.totalExecutionTime += executionTime;
|
|
636
|
+
eventMetrics.averageExecutionTime = eventMetrics.totalExecutionTime / eventMetrics.totalExecutions;
|
|
637
|
+
eventMetrics.lastExecutionTime = executionTime;
|
|
638
|
+
if (hadErrors) {
|
|
639
|
+
eventMetrics.errorCount++;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Update global metrics
|
|
643
|
+
this.globalMetrics.totalExecutions++;
|
|
644
|
+
this.globalMetrics.totalExecutionTime += executionTime;
|
|
645
|
+
this.globalMetrics.averageExecutionTime = this.globalMetrics.totalExecutionTime / this.globalMetrics.totalExecutions;
|
|
646
|
+
this.globalMetrics.lastExecutionTime = executionTime;
|
|
647
|
+
if (hadErrors) {
|
|
648
|
+
this.globalMetrics.errorCount++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Generate a unique hook ID
|
|
654
|
+
*/
|
|
655
|
+
private generateHookId(): string {
|
|
656
|
+
return `hook_${++this.hookCounter}_${Date.now()}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Check if an event matches the component targeting configuration
|
|
661
|
+
* @param event The lifecycle event
|
|
662
|
+
* @param componentTarget The component targeting configuration
|
|
663
|
+
* @returns True if the event matches the targeting criteria
|
|
664
|
+
*/
|
|
665
|
+
private matchesComponentTarget(event: LifecycleEvent, componentTarget?: ComponentTargetConfig): boolean {
|
|
666
|
+
// If no component targeting is specified, always match
|
|
667
|
+
if (!componentTarget) {
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const entity = event.getEntity();
|
|
672
|
+
const entityComponents = entity.componentList();
|
|
673
|
+
|
|
674
|
+
// Check archetype matching first (most specific)
|
|
675
|
+
if (componentTarget.archetype) {
|
|
676
|
+
if (!this.matchesArchetype(entityComponents, componentTarget.archetype, !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length))) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Check multiple archetypes (OR logic)
|
|
682
|
+
if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
|
|
683
|
+
const allowExtra = !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length);
|
|
684
|
+
const matchesAnyArchetype = componentTarget.archetypes.some(archetype =>
|
|
685
|
+
this.matchesArchetype(entityComponents, archetype, allowExtra)
|
|
686
|
+
);
|
|
687
|
+
if (!matchesAnyArchetype) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Check included components
|
|
693
|
+
if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
|
|
694
|
+
const includeMatch = this.checkComponentPresence(
|
|
695
|
+
entityComponents,
|
|
696
|
+
componentTarget.includeComponents,
|
|
697
|
+
componentTarget.requireAllIncluded ?? true
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
if (!includeMatch) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Check excluded components
|
|
706
|
+
if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
|
|
707
|
+
const excludeMatch = this.checkComponentAbsence(
|
|
708
|
+
entityComponents,
|
|
709
|
+
componentTarget.excludeComponents,
|
|
710
|
+
componentTarget.requireAllExcluded ?? true
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
if (!excludeMatch) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Check if required components are present on the entity
|
|
723
|
+
* @param entityComponents Array of component instances on the entity
|
|
724
|
+
* @param requiredComponents Array of component constructors to check for
|
|
725
|
+
* @param requireAll Whether to require ALL components (AND) or ANY component (OR)
|
|
726
|
+
* @returns True if the presence check passes
|
|
727
|
+
*/
|
|
728
|
+
private checkComponentPresence(
|
|
729
|
+
entityComponents: BaseComponent[],
|
|
730
|
+
requiredComponents: (new () => BaseComponent)[],
|
|
731
|
+
requireAll: boolean
|
|
732
|
+
): boolean {
|
|
733
|
+
const entityComponentTypes = new Set(
|
|
734
|
+
entityComponents.map(comp => comp.getTypeID())
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const requiredTypeIds = requiredComponents.map(compCtor => {
|
|
738
|
+
const instance = new compCtor();
|
|
739
|
+
return instance.getTypeID();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
if (requireAll) {
|
|
743
|
+
// ALL required components must be present (AND logic)
|
|
744
|
+
return requiredTypeIds.every(typeId => entityComponentTypes.has(typeId));
|
|
745
|
+
} else {
|
|
746
|
+
// ANY required component must be present (OR logic)
|
|
747
|
+
return requiredTypeIds.some(typeId => entityComponentTypes.has(typeId));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Check if excluded components are absent from the entity
|
|
753
|
+
* @param entityComponents Array of component instances on the entity
|
|
754
|
+
* @param excludedComponents Array of component constructors to check for absence
|
|
755
|
+
* @param requireAll Whether to require ALL components to be absent (AND) or ANY component to be absent (OR)
|
|
756
|
+
* @returns True if the absence check passes
|
|
757
|
+
*/
|
|
758
|
+
private checkComponentAbsence(
|
|
759
|
+
entityComponents: BaseComponent[],
|
|
760
|
+
excludedComponents: (new () => BaseComponent)[],
|
|
761
|
+
requireAll: boolean
|
|
762
|
+
): boolean {
|
|
763
|
+
const entityComponentTypes = new Set(
|
|
764
|
+
entityComponents.map(comp => comp.getTypeID())
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
const excludedTypeIds = excludedComponents.map(compCtor => {
|
|
768
|
+
const instance = new compCtor();
|
|
769
|
+
return instance.getTypeID();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
if (requireAll) {
|
|
773
|
+
// ALL excluded components must be absent (AND logic)
|
|
774
|
+
return excludedTypeIds.every(typeId => !entityComponentTypes.has(typeId));
|
|
775
|
+
} else {
|
|
776
|
+
// ANY excluded component must be absent (OR logic) - this is less common but supported
|
|
777
|
+
return excludedTypeIds.some(typeId => !entityComponentTypes.has(typeId));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Check if entity components match a specific archetype
|
|
783
|
+
* @param entityComponents Array of component instances on the entity
|
|
784
|
+
* @param archetype The archetype to match against
|
|
785
|
+
* @param allowExtraComponents Whether to allow additional components beyond the archetype
|
|
786
|
+
* @returns True if the entity matches the archetype
|
|
787
|
+
*/
|
|
788
|
+
private matchesArchetype(entityComponents: BaseComponent[], archetype: ArcheType, allowExtraComponents: boolean = false): boolean {
|
|
789
|
+
// Get the expected component types from the archetype
|
|
790
|
+
// We need to access the private componentMap from ArcheType
|
|
791
|
+
const archetypeComponentMap = (archetype as any).componentMap as Record<string, typeof BaseComponent>;
|
|
792
|
+
|
|
793
|
+
if (!archetypeComponentMap) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const expectedComponentTypes = new Set(
|
|
798
|
+
Object.values(archetypeComponentMap).map(compCtor => {
|
|
799
|
+
const instance = new compCtor();
|
|
800
|
+
return instance.getTypeID();
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const entityComponentTypes = new Set(
|
|
805
|
+
entityComponents.map(comp => comp.getTypeID())
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
if (allowExtraComponents) {
|
|
809
|
+
// Entity must have at least all the component types from the archetype
|
|
810
|
+
// (allows additional components beyond the archetype)
|
|
811
|
+
for (const expectedType of expectedComponentTypes) {
|
|
812
|
+
if (!entityComponentTypes.has(expectedType)) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return true;
|
|
817
|
+
} else {
|
|
818
|
+
// Entity must have exactly the same component types as the archetype
|
|
819
|
+
if (expectedComponentTypes.size !== entityComponentTypes.size) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// All expected component types must be present in the entity
|
|
824
|
+
for (const expectedType of expectedComponentTypes) {
|
|
825
|
+
if (!entityComponentTypes.has(expectedType)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Sort hooks by priority (higher priority first)
|
|
835
|
+
*/
|
|
836
|
+
private sortHooksByPriority(eventType: string): void {
|
|
837
|
+
const hooks = this.hooks.get(eventType);
|
|
838
|
+
if (hooks) {
|
|
839
|
+
hooks.sort((a, b) => (b.options.priority || 0) - (a.options.priority || 0));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Get the singleton instance of EntityHookManager
|
|
845
|
+
*/
|
|
846
|
+
public static get instance(): EntityHookManager {
|
|
847
|
+
if (!EntityHookManager._instance) {
|
|
848
|
+
EntityHookManager._instance = new EntityHookManager();
|
|
849
|
+
}
|
|
850
|
+
return EntityHookManager._instance;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Export singleton instance
|
|
855
|
+
export default EntityHookManager.instance;
|