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.
Files changed (85) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +119 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +159 -12
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +453 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +65 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +1 -1
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/examples/hooks/README.md +228 -0
  52. package/examples/hooks/audit-logger.ts +495 -0
  53. package/gql/Generator.ts +56 -34
  54. package/gql/decorators/Upload.ts +176 -0
  55. package/gql/helpers.ts +67 -0
  56. package/gql/index.ts +55 -31
  57. package/gql/types.ts +1 -1
  58. package/index.ts +79 -11
  59. package/package.json +5 -4
  60. package/rest/Generator.ts +3 -0
  61. package/rest/index.ts +22 -0
  62. package/service/Service.ts +1 -1
  63. package/service/ServiceRegistry.ts +10 -6
  64. package/service/index.ts +12 -1
  65. package/tests/bench/insert.bench.ts +59 -0
  66. package/tests/bench/relations.bench.ts +269 -0
  67. package/tests/bench/sorting.bench.ts +415 -0
  68. package/tests/component-hooks.test.ts +1409 -0
  69. package/tests/component.test.ts +205 -0
  70. package/tests/errorHandling.test.ts +155 -0
  71. package/tests/hooks.test.ts +666 -0
  72. package/tests/query-sorting.test.ts +101 -0
  73. package/tests/relations.test.ts +169 -0
  74. package/tests/scheduler.test.ts +724 -0
  75. package/tsconfig.json +35 -34
  76. package/types/graphql.types.ts +87 -0
  77. package/types/hooks.types.ts +141 -0
  78. package/types/scheduler.types.ts +165 -0
  79. package/types/upload.types.ts +184 -0
  80. package/upload/index.ts +140 -0
  81. package/utils/UploadHelper.ts +305 -0
  82. package/utils/cronParser.ts +366 -0
  83. package/utils/errorMessages.ts +151 -0
  84. package/validate-docs.sh +90 -0
  85. 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;