bunsane 0.3.2 → 0.5.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/CHANGELOG.md +471 -370
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +93 -1020
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +94 -85
- package/core/RequestLoaders.ts +98 -5
- package/core/SchedulerManager.ts +28 -600
- package/core/app/cors.ts +2 -11
- package/core/app/preparedStatementWarmup.ts +9 -49
- package/core/app/requestRouter.ts +9 -8
- package/core/app/restRegistry.ts +8 -0
- package/core/archetype/fieldResolvers.ts +85 -40
- package/core/archetype/relationLoader.ts +135 -92
- package/core/cache/CacheManager.ts +91 -302
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +8 -7
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +25 -10
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +383 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/getCacheManager.ts +10 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +375 -0
- package/core/health.ts +93 -4
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/StreamConsumer.ts +535 -535
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/core/validateEnv.ts +10 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +8 -2
- package/database/cancellable.ts +35 -22
- package/database/index.ts +29 -3
- package/database/instrumentedDb.ts +141 -141
- package/database/sqlHelpers.ts +3 -1
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/schema/index.ts +15 -4
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +245 -14
- package/query/OrNode.ts +8 -19
- package/query/Query.ts +208 -79
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/RFC_APP_REFACTOR.md +0 -248
- package/docs/RFC_REFACTOR_TARGETS.md +0 -251
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
- package/tests/integration/query/Query.abort.test.ts +0 -66
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -498
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/database/cancellable.test.ts +0 -81
- package/tests/unit/database/instrumentedDb.test.ts +0 -160
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- package/tests/utils/test-context.ts +0 -149
|
@@ -1,112 +1,52 @@
|
|
|
1
|
-
import type {
|
|
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";
|
|
1
|
+
import type { EntityEvent, ComponentEvent, LifecycleEvent } from "./events/EntityLifecycleEvents";
|
|
17
2
|
import { logger as MainLogger } from "./Logger";
|
|
18
3
|
import ApplicationLifecycle, { ApplicationPhase, type PhaseChangeEvent } from "./ApplicationLifecycle";
|
|
4
|
+
import {
|
|
5
|
+
type EntityHookCallback,
|
|
6
|
+
type ComponentHookCallback,
|
|
7
|
+
type LifecycleHookCallback,
|
|
8
|
+
type ComponentTargetConfig,
|
|
9
|
+
type HookOptions,
|
|
10
|
+
type HookMetrics,
|
|
11
|
+
type RegistryState,
|
|
12
|
+
createRegistryState,
|
|
13
|
+
registerEntityHook,
|
|
14
|
+
registerComponentHook,
|
|
15
|
+
registerLifecycleHook,
|
|
16
|
+
removeHook,
|
|
17
|
+
getHookCount,
|
|
18
|
+
clearAllHooks
|
|
19
|
+
} from "./hooks/registry";
|
|
20
|
+
import {
|
|
21
|
+
type DispatcherState,
|
|
22
|
+
createDispatcherState,
|
|
23
|
+
executeHooks,
|
|
24
|
+
executeHooksBatch,
|
|
25
|
+
getMetrics,
|
|
26
|
+
resetMetrics
|
|
27
|
+
} from "./hooks/dispatcher";
|
|
28
|
+
|
|
29
|
+
// Re-export types consumed by external modules
|
|
30
|
+
export type {
|
|
31
|
+
EntityHookCallback,
|
|
32
|
+
ComponentHookCallback,
|
|
33
|
+
LifecycleHookCallback,
|
|
34
|
+
ComponentTargetConfig,
|
|
35
|
+
HookOptions,
|
|
36
|
+
HookMetrics
|
|
37
|
+
};
|
|
19
38
|
|
|
20
39
|
const logger = MainLogger.child({ scope: "EntityHookManager" });
|
|
21
40
|
|
|
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
41
|
/**
|
|
94
42
|
* EntityHookManager - Singleton for managing entity lifecycle hooks
|
|
95
43
|
* Provides registration and execution of hooks for entity and component lifecycle events
|
|
96
44
|
*/
|
|
97
45
|
class EntityHookManager {
|
|
98
46
|
private static _instance: EntityHookManager;
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private metrics: Map<string, HookMetrics> = new Map();
|
|
47
|
+
private registryState: RegistryState = createRegistryState();
|
|
48
|
+
private dispatcherState: DispatcherState = createDispatcherState();
|
|
102
49
|
private phaseListener: ((event: PhaseChangeEvent) => void) | null = null;
|
|
103
|
-
private globalMetrics: HookMetrics = {
|
|
104
|
-
totalExecutions: 0,
|
|
105
|
-
totalExecutionTime: 0,
|
|
106
|
-
averageExecutionTime: 0,
|
|
107
|
-
errorCount: 0,
|
|
108
|
-
lastExecutionTime: 0
|
|
109
|
-
};
|
|
110
50
|
|
|
111
51
|
private constructor() {
|
|
112
52
|
logger.trace("EntityHookManager initialized");
|
|
@@ -165,20 +105,7 @@ class EntityHookManager {
|
|
|
165
105
|
callback: EntityHookCallback<T>,
|
|
166
106
|
options: HookOptions = {}
|
|
167
107
|
): string {
|
|
168
|
-
const hookId = this.
|
|
169
|
-
const hook: RegisteredHook = {
|
|
170
|
-
callback: callback as LifecycleHookCallback,
|
|
171
|
-
options: { priority: 0, ...options },
|
|
172
|
-
id: hookId
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
if (!this.hooks.has(eventType)) {
|
|
176
|
-
this.hooks.set(eventType, []);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
this.hooks.get(eventType)!.push(hook);
|
|
180
|
-
this.sortHooksByPriority(eventType);
|
|
181
|
-
|
|
108
|
+
const hookId = registerEntityHook(this.registryState, eventType, callback, options);
|
|
182
109
|
logger.trace(`Registered entity hook ${hookId} for event type: ${eventType}`);
|
|
183
110
|
return hookId;
|
|
184
111
|
}
|
|
@@ -195,20 +122,7 @@ class EntityHookManager {
|
|
|
195
122
|
callback: ComponentHookCallback<T>,
|
|
196
123
|
options: HookOptions = {}
|
|
197
124
|
): string {
|
|
198
|
-
const hookId = this.
|
|
199
|
-
const hook: RegisteredHook = {
|
|
200
|
-
callback: callback as LifecycleHookCallback,
|
|
201
|
-
options: { priority: 0, ...options },
|
|
202
|
-
id: hookId
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
if (!this.hooks.has(eventType)) {
|
|
206
|
-
this.hooks.set(eventType, []);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
this.hooks.get(eventType)!.push(hook);
|
|
210
|
-
this.sortHooksByPriority(eventType);
|
|
211
|
-
|
|
125
|
+
const hookId = registerComponentHook(this.registryState, eventType, callback, options);
|
|
212
126
|
logger.trace(`Registered component hook ${hookId} for event type: ${eventType}`);
|
|
213
127
|
return hookId;
|
|
214
128
|
}
|
|
@@ -223,26 +137,7 @@ class EntityHookManager {
|
|
|
223
137
|
callback: LifecycleHookCallback,
|
|
224
138
|
options: HookOptions = {}
|
|
225
139
|
): string {
|
|
226
|
-
const hookId = this.
|
|
227
|
-
const hook: RegisteredHook = {
|
|
228
|
-
callback,
|
|
229
|
-
options: { priority: 0, ...options },
|
|
230
|
-
id: hookId
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Register for all event types
|
|
234
|
-
const allEventTypes = [
|
|
235
|
-
"entity.created", "entity.updated", "entity.deleted",
|
|
236
|
-
"component.added", "component.updated", "component.removed"
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
for (const eventType of allEventTypes) {
|
|
240
|
-
if (!this.hooks.has(eventType)) {
|
|
241
|
-
this.hooks.set(eventType, []);
|
|
242
|
-
}
|
|
243
|
-
this.hooks.get(eventType)!.push({ ...hook }); // Clone hook for each event type
|
|
244
|
-
}
|
|
245
|
-
|
|
140
|
+
const hookId = registerLifecycleHook(this.registryState, callback, options);
|
|
246
141
|
logger.trace(`Registered lifecycle hook ${hookId} for all event types`);
|
|
247
142
|
return hookId;
|
|
248
143
|
}
|
|
@@ -253,18 +148,10 @@ class EntityHookManager {
|
|
|
253
148
|
* @returns True if hook was removed, false if not found
|
|
254
149
|
*/
|
|
255
150
|
public removeHook(hookId: string): boolean {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const initialLength = hooks.length;
|
|
260
|
-
this.hooks.set(eventType, hooks.filter(hook => hook.id !== hookId));
|
|
261
|
-
|
|
262
|
-
if (this.hooks.get(eventType)!.length < initialLength) {
|
|
263
|
-
removed = true;
|
|
264
|
-
logger.trace(`Removed hook ${hookId} from event type: ${eventType}`);
|
|
265
|
-
}
|
|
151
|
+
const removed = removeHook(this.registryState, hookId);
|
|
152
|
+
if (removed) {
|
|
153
|
+
logger.trace(`Removed hook ${hookId}`);
|
|
266
154
|
}
|
|
267
|
-
|
|
268
155
|
return removed;
|
|
269
156
|
}
|
|
270
157
|
|
|
@@ -273,345 +160,15 @@ class EntityHookManager {
|
|
|
273
160
|
* @param event The lifecycle event to process
|
|
274
161
|
*/
|
|
275
162
|
public async executeHooks(event: LifecycleEvent): Promise<void> {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const startTime = performance.now();
|
|
279
|
-
let hadErrors = false;
|
|
280
|
-
|
|
281
|
-
if (hooks.length === 0) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
logger.trace(`Executing ${hooks.length} hooks for event: ${eventType}`);
|
|
286
|
-
|
|
287
|
-
// Separate sync and async hooks
|
|
288
|
-
const syncHooks = hooks.filter(hook => !hook.options.async);
|
|
289
|
-
const asyncHooks = hooks.filter(hook => hook.options.async);
|
|
290
|
-
|
|
291
|
-
// Execute sync hooks immediately
|
|
292
|
-
for (const hook of syncHooks) {
|
|
293
|
-
// Check component targeting first
|
|
294
|
-
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Check filter condition
|
|
299
|
-
if (hook.options.filter && !hook.options.filter(event)) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
305
|
-
// Execute with timeout. Timer handle is stored so the
|
|
306
|
-
// normal-completion path clears it (no leaked pending
|
|
307
|
-
// timers per successful hook). The underlying callback
|
|
308
|
-
// promise is attached with a detached .catch so a late
|
|
309
|
-
// rejection after timeout does not escape as unhandled
|
|
310
|
-
// (H-HOOK-2 / H-MEM-2).
|
|
311
|
-
let timerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
312
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
313
|
-
timerHandle = setTimeout(
|
|
314
|
-
() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
|
|
315
|
-
hook.options.timeout
|
|
316
|
-
);
|
|
317
|
-
});
|
|
318
|
-
const hookPromise = Promise.resolve().then(() => hook.callback(event));
|
|
319
|
-
hookPromise.catch((err) => {
|
|
320
|
-
logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
|
|
321
|
-
});
|
|
322
|
-
try {
|
|
323
|
-
await Promise.race([hookPromise, timeoutPromise]);
|
|
324
|
-
} finally {
|
|
325
|
-
if (timerHandle) clearTimeout(timerHandle);
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
// Always await — callback may be an async function declared
|
|
329
|
-
// with async:false by mistake. Without await, a rejection
|
|
330
|
-
// from such a callback escapes as an unhandled rejection
|
|
331
|
-
// and crashes the process under strict mode (C13).
|
|
332
|
-
await hook.callback(event);
|
|
333
|
-
}
|
|
334
|
-
} catch (error) {
|
|
335
|
-
logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
|
|
336
|
-
hadErrors = true;
|
|
337
|
-
// Continue executing other hooks even if one fails
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Execute async hooks in parallel
|
|
342
|
-
if (asyncHooks.length > 0) {
|
|
343
|
-
const asyncPromises = asyncHooks.map(async (hook) => {
|
|
344
|
-
// Check component targeting first
|
|
345
|
-
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Check filter condition
|
|
350
|
-
if (hook.options.filter && !hook.options.filter(event)) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
356
|
-
// Execute with timeout. See sync path for rationale —
|
|
357
|
-
// clear the timer on normal completion and detach a
|
|
358
|
-
// .catch on the hook promise so late rejections do
|
|
359
|
-
// not escape (H-HOOK-2 / H-MEM-2).
|
|
360
|
-
let timerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
361
|
-
const hookPromise = Promise.resolve().then(() => hook.callback(event));
|
|
362
|
-
hookPromise.catch((err) => {
|
|
363
|
-
logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
|
|
364
|
-
});
|
|
365
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
366
|
-
timerHandle = setTimeout(
|
|
367
|
-
() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
|
|
368
|
-
hook.options.timeout
|
|
369
|
-
);
|
|
370
|
-
});
|
|
371
|
-
try {
|
|
372
|
-
await Promise.race([hookPromise, timeoutPromise]);
|
|
373
|
-
} finally {
|
|
374
|
-
if (timerHandle) clearTimeout(timerHandle);
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
// Execute normally
|
|
378
|
-
await hook.callback(event);
|
|
379
|
-
}
|
|
380
|
-
} catch (error) {
|
|
381
|
-
logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
|
|
382
|
-
hadErrors = true;
|
|
383
|
-
// Continue executing other hooks even if one fails
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
await Promise.allSettled(asyncPromises);
|
|
388
|
-
}
|
|
163
|
+
return executeHooks(this.registryState, this.dispatcherState, event);
|
|
164
|
+
}
|
|
389
165
|
|
|
390
|
-
|
|
391
|
-
const executionTime = performance.now() - startTime;
|
|
392
|
-
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
393
|
-
} /**
|
|
166
|
+
/**
|
|
394
167
|
* Execute hooks for multiple events in batch
|
|
395
168
|
* @param events Array of lifecycle events to process
|
|
396
169
|
*/
|
|
397
170
|
public async executeHooksBatch(events: LifecycleEvent[]): Promise<void> {
|
|
398
|
-
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
logger.trace(`Executing hooks for ${events.length} events in batch`);
|
|
403
|
-
|
|
404
|
-
// Group events by type for efficient processing
|
|
405
|
-
const eventsByType = new Map<string, LifecycleEvent[]>();
|
|
406
|
-
for (const event of events) {
|
|
407
|
-
const eventType = event.getEventType();
|
|
408
|
-
if (!eventsByType.has(eventType)) {
|
|
409
|
-
eventsByType.set(eventType, []);
|
|
410
|
-
}
|
|
411
|
-
eventsByType.get(eventType)!.push(event);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Process each event type
|
|
415
|
-
const promises: Promise<void>[] = [];
|
|
416
|
-
for (const [eventType, typeEvents] of eventsByType.entries()) {
|
|
417
|
-
promises.push(this.executeHooksForType(eventType, typeEvents));
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
await Promise.allSettled(promises);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Execute hooks for a specific event type with multiple events
|
|
425
|
-
* @param eventType The event type
|
|
426
|
-
* @param events Array of events of the same type
|
|
427
|
-
*/
|
|
428
|
-
private async executeHooksForType(eventType: string, events: LifecycleEvent[]): Promise<void> {
|
|
429
|
-
const hooks = this.hooks.get(eventType) || [];
|
|
430
|
-
|
|
431
|
-
if (hooks.length === 0 || events.length === 0) {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
logger.trace(`Executing ${hooks.length} hooks for ${events.length} ${eventType} events`);
|
|
436
|
-
|
|
437
|
-
// Pre-filter hooks by component targeting to avoid repeated checks
|
|
438
|
-
const preFilteredHooks = this.preFilterHooksByComponentTargeting(hooks, events);
|
|
439
|
-
|
|
440
|
-
if (preFilteredHooks.length === 0) {
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Separate sync and async hooks
|
|
445
|
-
const syncHooks = preFilteredHooks.filter(hook => !hook.options.async);
|
|
446
|
-
const asyncHooks = preFilteredHooks.filter(hook => hook.options.async);
|
|
447
|
-
|
|
448
|
-
// Execute sync hooks for all events with batch optimization
|
|
449
|
-
if (syncHooks.length > 0) {
|
|
450
|
-
await this.executeSyncHooksBatch(syncHooks, events, eventType);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Execute async hooks in parallel for all events with batch optimization
|
|
454
|
-
if (asyncHooks.length > 0) {
|
|
455
|
-
await this.executeAsyncHooksBatch(asyncHooks, events, eventType);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Pre-filter hooks based on component targeting to optimize batch processing
|
|
461
|
-
* @param hooks Array of hooks to filter
|
|
462
|
-
* @param events Array of events to check against
|
|
463
|
-
* @returns Array of hooks that could potentially match any of the events
|
|
464
|
-
*/
|
|
465
|
-
private preFilterHooksByComponentTargeting(hooks: RegisteredHook[], events: LifecycleEvent[]): RegisteredHook[] {
|
|
466
|
-
// If no hooks have component targeting, return all hooks (preserving order)
|
|
467
|
-
const hasComponentTargeting = hooks.some(hook => hook.options.componentTarget);
|
|
468
|
-
if (!hasComponentTargeting) {
|
|
469
|
-
return [...hooks]; // Return a copy to avoid modifying the original
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// For hooks with component targeting, check if they could match any event
|
|
473
|
-
// This is a broad pre-filter to avoid checking every hook against every event
|
|
474
|
-
const filteredHooks = hooks.filter(hook => {
|
|
475
|
-
if (!hook.options.componentTarget) {
|
|
476
|
-
return true; // No targeting means it matches all
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Check if this hook could potentially match any of the events
|
|
480
|
-
return events.some(event => this.matchesComponentTarget(event, hook.options.componentTarget));
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
// Return filtered hooks in their original order (priority should already be sorted)
|
|
484
|
-
return filteredHooks;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Execute sync hooks for multiple events with batch optimizations
|
|
489
|
-
* @param syncHooks Array of synchronous hooks
|
|
490
|
-
* @param events Array of events
|
|
491
|
-
* @param eventType The event type
|
|
492
|
-
*/
|
|
493
|
-
private async executeSyncHooksBatch(syncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
|
|
494
|
-
const startTime = performance.now();
|
|
495
|
-
let hadErrors = false;
|
|
496
|
-
|
|
497
|
-
// Execute hooks in priority order across all events to maintain deterministic execution
|
|
498
|
-
for (const hook of syncHooks) {
|
|
499
|
-
// Process all events for this hook
|
|
500
|
-
for (const event of events) {
|
|
501
|
-
// Double-check component targeting (pre-filter may have false positives)
|
|
502
|
-
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Check filter condition
|
|
507
|
-
if (hook.options.filter && !hook.options.filter(event)) {
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
try {
|
|
512
|
-
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
513
|
-
// Same cleanup pattern as single-event path (H-HOOK-2 / H-MEM-2).
|
|
514
|
-
let timerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
515
|
-
const hookPromise = Promise.resolve().then(() => hook.callback(event));
|
|
516
|
-
hookPromise.catch((err) => {
|
|
517
|
-
logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
|
|
518
|
-
});
|
|
519
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
520
|
-
timerHandle = setTimeout(
|
|
521
|
-
() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
|
|
522
|
-
hook.options.timeout
|
|
523
|
-
);
|
|
524
|
-
});
|
|
525
|
-
try {
|
|
526
|
-
await Promise.race([hookPromise, timeoutPromise]);
|
|
527
|
-
} finally {
|
|
528
|
-
if (timerHandle) clearTimeout(timerHandle);
|
|
529
|
-
}
|
|
530
|
-
} else {
|
|
531
|
-
// Await so async callbacks do not escape as unhandled
|
|
532
|
-
// rejections (C13 parity).
|
|
533
|
-
await hook.callback(event);
|
|
534
|
-
}
|
|
535
|
-
} catch (error) {
|
|
536
|
-
logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
|
|
537
|
-
hadErrors = true;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Record performance metrics
|
|
543
|
-
const executionTime = performance.now() - startTime;
|
|
544
|
-
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Execute async hooks for multiple events with batch optimizations
|
|
549
|
-
* @param asyncHooks Array of asynchronous hooks
|
|
550
|
-
* @param events Array of events
|
|
551
|
-
* @param eventType The event type
|
|
552
|
-
*/
|
|
553
|
-
private async executeAsyncHooksBatch(asyncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
|
|
554
|
-
const startTime = performance.now();
|
|
555
|
-
let hadErrors = false;
|
|
556
|
-
|
|
557
|
-
// Collect all async hook executions
|
|
558
|
-
const asyncPromises: Promise<void>[] = [];
|
|
559
|
-
|
|
560
|
-
// Use a more efficient batching strategy for async hooks
|
|
561
|
-
for (const event of events) {
|
|
562
|
-
for (const hook of asyncHooks) {
|
|
563
|
-
// Double-check component targeting
|
|
564
|
-
if (!this.matchesComponentTarget(event, hook.options.componentTarget)) {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Check filter condition
|
|
569
|
-
if (hook.options.filter && !hook.options.filter(event)) {
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
asyncPromises.push(
|
|
574
|
-
(async () => {
|
|
575
|
-
try {
|
|
576
|
-
if (hook.options.timeout && hook.options.timeout > 0) {
|
|
577
|
-
// Same cleanup pattern (H-HOOK-2 / H-MEM-2).
|
|
578
|
-
let timerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
579
|
-
const hookPromise = Promise.resolve().then(() => hook.callback(event));
|
|
580
|
-
hookPromise.catch((err) => {
|
|
581
|
-
logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
|
|
582
|
-
});
|
|
583
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
584
|
-
timerHandle = setTimeout(
|
|
585
|
-
() => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
|
|
586
|
-
hook.options.timeout
|
|
587
|
-
);
|
|
588
|
-
});
|
|
589
|
-
try {
|
|
590
|
-
await Promise.race([hookPromise, timeoutPromise]);
|
|
591
|
-
} finally {
|
|
592
|
-
if (timerHandle) clearTimeout(timerHandle);
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
// Execute normally
|
|
596
|
-
await hook.callback(event);
|
|
597
|
-
}
|
|
598
|
-
} catch (error) {
|
|
599
|
-
logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
|
|
600
|
-
hadErrors = true;
|
|
601
|
-
}
|
|
602
|
-
})()
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Execute all async hooks in parallel with controlled concurrency
|
|
608
|
-
if (asyncPromises.length > 0) {
|
|
609
|
-
await Promise.allSettled(asyncPromises);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Record performance metrics
|
|
613
|
-
const executionTime = performance.now() - startTime;
|
|
614
|
-
this.recordMetrics(eventType, executionTime, hadErrors);
|
|
171
|
+
return executeHooksBatch(this.registryState, this.dispatcherState, events);
|
|
615
172
|
}
|
|
616
173
|
|
|
617
174
|
/**
|
|
@@ -620,15 +177,7 @@ class EntityHookManager {
|
|
|
620
177
|
* @returns Number of registered hooks
|
|
621
178
|
*/
|
|
622
179
|
public getHookCount(eventType?: string): number {
|
|
623
|
-
|
|
624
|
-
return this.hooks.get(eventType)?.length || 0;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
let total = 0;
|
|
628
|
-
for (const hooks of this.hooks.values()) {
|
|
629
|
-
total += hooks.length;
|
|
630
|
-
}
|
|
631
|
-
return total;
|
|
180
|
+
return getHookCount(this.registryState, eventType);
|
|
632
181
|
}
|
|
633
182
|
|
|
634
183
|
/**
|
|
@@ -637,16 +186,7 @@ class EntityHookManager {
|
|
|
637
186
|
* @returns Hook execution metrics
|
|
638
187
|
*/
|
|
639
188
|
public getMetrics(eventType?: string): HookMetrics {
|
|
640
|
-
|
|
641
|
-
return this.metrics.get(eventType) || {
|
|
642
|
-
totalExecutions: 0,
|
|
643
|
-
totalExecutionTime: 0,
|
|
644
|
-
averageExecutionTime: 0,
|
|
645
|
-
errorCount: 0,
|
|
646
|
-
lastExecutionTime: 0
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
return { ...this.globalMetrics };
|
|
189
|
+
return getMetrics(this.dispatcherState, eventType);
|
|
650
190
|
}
|
|
651
191
|
|
|
652
192
|
/**
|
|
@@ -654,259 +194,17 @@ class EntityHookManager {
|
|
|
654
194
|
* @param eventType Optional event type to reset specific metrics
|
|
655
195
|
*/
|
|
656
196
|
public resetMetrics(eventType?: string): void {
|
|
657
|
-
|
|
658
|
-
this.metrics.delete(eventType);
|
|
659
|
-
} else {
|
|
660
|
-
this.metrics.clear();
|
|
661
|
-
this.globalMetrics = {
|
|
662
|
-
totalExecutions: 0,
|
|
663
|
-
totalExecutionTime: 0,
|
|
664
|
-
averageExecutionTime: 0,
|
|
665
|
-
errorCount: 0,
|
|
666
|
-
lastExecutionTime: 0
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
logger.trace(`Reset metrics${eventType ? ` for ${eventType}` : ''}`);
|
|
197
|
+
resetMetrics(this.dispatcherState, eventType);
|
|
670
198
|
}
|
|
671
199
|
|
|
672
200
|
/**
|
|
673
201
|
* Clear all hooks (useful for testing)
|
|
674
202
|
*/
|
|
675
203
|
public clearAllHooks(): void {
|
|
676
|
-
this.
|
|
677
|
-
this.hookCounter = 0;
|
|
204
|
+
clearAllHooks(this.registryState);
|
|
678
205
|
logger.trace("Cleared all hooks");
|
|
679
206
|
}
|
|
680
207
|
|
|
681
|
-
/**
|
|
682
|
-
* Record hook execution metrics
|
|
683
|
-
* @param eventType The event type
|
|
684
|
-
* @param executionTime Time taken to execute hooks
|
|
685
|
-
* @param hadErrors Whether any hooks had errors
|
|
686
|
-
*/
|
|
687
|
-
private recordMetrics(eventType: string, executionTime: number, hadErrors: boolean): void {
|
|
688
|
-
// Update event-specific metrics
|
|
689
|
-
let eventMetrics = this.metrics.get(eventType);
|
|
690
|
-
if (!eventMetrics) {
|
|
691
|
-
eventMetrics = {
|
|
692
|
-
totalExecutions: 0,
|
|
693
|
-
totalExecutionTime: 0,
|
|
694
|
-
averageExecutionTime: 0,
|
|
695
|
-
errorCount: 0,
|
|
696
|
-
lastExecutionTime: 0
|
|
697
|
-
};
|
|
698
|
-
this.metrics.set(eventType, eventMetrics);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
eventMetrics.totalExecutions++;
|
|
702
|
-
eventMetrics.totalExecutionTime += executionTime;
|
|
703
|
-
eventMetrics.averageExecutionTime = eventMetrics.totalExecutionTime / eventMetrics.totalExecutions;
|
|
704
|
-
eventMetrics.lastExecutionTime = executionTime;
|
|
705
|
-
if (hadErrors) {
|
|
706
|
-
eventMetrics.errorCount++;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Update global metrics
|
|
710
|
-
this.globalMetrics.totalExecutions++;
|
|
711
|
-
this.globalMetrics.totalExecutionTime += executionTime;
|
|
712
|
-
this.globalMetrics.averageExecutionTime = this.globalMetrics.totalExecutionTime / this.globalMetrics.totalExecutions;
|
|
713
|
-
this.globalMetrics.lastExecutionTime = executionTime;
|
|
714
|
-
if (hadErrors) {
|
|
715
|
-
this.globalMetrics.errorCount++;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Generate a unique hook ID
|
|
721
|
-
*/
|
|
722
|
-
private generateHookId(): string {
|
|
723
|
-
return `hook_${++this.hookCounter}_${Date.now()}`;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Check if an event matches the component targeting configuration
|
|
728
|
-
* @param event The lifecycle event
|
|
729
|
-
* @param componentTarget The component targeting configuration
|
|
730
|
-
* @returns True if the event matches the targeting criteria
|
|
731
|
-
*/
|
|
732
|
-
private matchesComponentTarget(event: LifecycleEvent, componentTarget?: ComponentTargetConfig): boolean {
|
|
733
|
-
// If no component targeting is specified, always match
|
|
734
|
-
if (!componentTarget) {
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const entity = event.getEntity();
|
|
739
|
-
const entityComponents = entity.componentList();
|
|
740
|
-
|
|
741
|
-
// Check archetype matching first (most specific)
|
|
742
|
-
if (componentTarget.archetype) {
|
|
743
|
-
if (!this.matchesArchetype(entityComponents, componentTarget.archetype, !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length))) {
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Check multiple archetypes (OR logic)
|
|
749
|
-
if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
|
|
750
|
-
const allowExtra = !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length);
|
|
751
|
-
const matchesAnyArchetype = componentTarget.archetypes.some(archetype =>
|
|
752
|
-
this.matchesArchetype(entityComponents, archetype, allowExtra)
|
|
753
|
-
);
|
|
754
|
-
if (!matchesAnyArchetype) {
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Check included components
|
|
760
|
-
if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
|
|
761
|
-
const includeMatch = this.checkComponentPresence(
|
|
762
|
-
entityComponents,
|
|
763
|
-
componentTarget.includeComponents,
|
|
764
|
-
componentTarget.requireAllIncluded ?? true
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
if (!includeMatch) {
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Check excluded components
|
|
773
|
-
if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
|
|
774
|
-
const excludeMatch = this.checkComponentAbsence(
|
|
775
|
-
entityComponents,
|
|
776
|
-
componentTarget.excludeComponents,
|
|
777
|
-
componentTarget.requireAllExcluded ?? true
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
if (!excludeMatch) {
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return true;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Check if required components are present on the entity
|
|
790
|
-
* @param entityComponents Array of component instances on the entity
|
|
791
|
-
* @param requiredComponents Array of component constructors to check for
|
|
792
|
-
* @param requireAll Whether to require ALL components (AND) or ANY component (OR)
|
|
793
|
-
* @returns True if the presence check passes
|
|
794
|
-
*/
|
|
795
|
-
private checkComponentPresence(
|
|
796
|
-
entityComponents: BaseComponent[],
|
|
797
|
-
requiredComponents: (new () => BaseComponent)[],
|
|
798
|
-
requireAll: boolean
|
|
799
|
-
): boolean {
|
|
800
|
-
const entityComponentTypes = new Set(
|
|
801
|
-
entityComponents.map(comp => comp.getTypeID())
|
|
802
|
-
);
|
|
803
|
-
|
|
804
|
-
const requiredTypeIds = requiredComponents.map(compCtor => {
|
|
805
|
-
const instance = new compCtor();
|
|
806
|
-
return instance.getTypeID();
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
if (requireAll) {
|
|
810
|
-
// ALL required components must be present (AND logic)
|
|
811
|
-
return requiredTypeIds.every(typeId => entityComponentTypes.has(typeId));
|
|
812
|
-
} else {
|
|
813
|
-
// ANY required component must be present (OR logic)
|
|
814
|
-
return requiredTypeIds.some(typeId => entityComponentTypes.has(typeId));
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Check if excluded components are absent from the entity
|
|
820
|
-
* @param entityComponents Array of component instances on the entity
|
|
821
|
-
* @param excludedComponents Array of component constructors to check for absence
|
|
822
|
-
* @param requireAll Whether to require ALL components to be absent (AND) or ANY component to be absent (OR)
|
|
823
|
-
* @returns True if the absence check passes
|
|
824
|
-
*/
|
|
825
|
-
private checkComponentAbsence(
|
|
826
|
-
entityComponents: BaseComponent[],
|
|
827
|
-
excludedComponents: (new () => BaseComponent)[],
|
|
828
|
-
requireAll: boolean
|
|
829
|
-
): boolean {
|
|
830
|
-
const entityComponentTypes = new Set(
|
|
831
|
-
entityComponents.map(comp => comp.getTypeID())
|
|
832
|
-
);
|
|
833
|
-
|
|
834
|
-
const excludedTypeIds = excludedComponents.map(compCtor => {
|
|
835
|
-
const instance = new compCtor();
|
|
836
|
-
return instance.getTypeID();
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
if (requireAll) {
|
|
840
|
-
// ALL excluded components must be absent (AND logic)
|
|
841
|
-
return excludedTypeIds.every(typeId => !entityComponentTypes.has(typeId));
|
|
842
|
-
} else {
|
|
843
|
-
// ANY excluded component must be absent (OR logic) - this is less common but supported
|
|
844
|
-
return excludedTypeIds.some(typeId => !entityComponentTypes.has(typeId));
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Check if entity components match a specific archetype
|
|
850
|
-
* @param entityComponents Array of component instances on the entity
|
|
851
|
-
* @param archetype The archetype to match against
|
|
852
|
-
* @param allowExtraComponents Whether to allow additional components beyond the archetype
|
|
853
|
-
* @returns True if the entity matches the archetype
|
|
854
|
-
*/
|
|
855
|
-
private matchesArchetype(entityComponents: BaseComponent[], archetype: ArcheType, allowExtraComponents: boolean = false): boolean {
|
|
856
|
-
// Get the expected component types from the archetype
|
|
857
|
-
// We need to access the private componentMap from ArcheType
|
|
858
|
-
const archetypeComponentMap = (archetype as any).componentMap as Record<string, typeof BaseComponent>;
|
|
859
|
-
|
|
860
|
-
if (!archetypeComponentMap) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const expectedComponentTypes = new Set(
|
|
865
|
-
Object.values(archetypeComponentMap).map(compCtor => {
|
|
866
|
-
const instance = new compCtor();
|
|
867
|
-
return instance.getTypeID();
|
|
868
|
-
})
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
const entityComponentTypes = new Set(
|
|
872
|
-
entityComponents.map(comp => comp.getTypeID())
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
if (allowExtraComponents) {
|
|
876
|
-
// Entity must have at least all the component types from the archetype
|
|
877
|
-
// (allows additional components beyond the archetype)
|
|
878
|
-
for (const expectedType of expectedComponentTypes) {
|
|
879
|
-
if (!entityComponentTypes.has(expectedType)) {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return true;
|
|
884
|
-
} else {
|
|
885
|
-
// Entity must have exactly the same component types as the archetype
|
|
886
|
-
if (expectedComponentTypes.size !== entityComponentTypes.size) {
|
|
887
|
-
return false;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// All expected component types must be present in the entity
|
|
891
|
-
for (const expectedType of expectedComponentTypes) {
|
|
892
|
-
if (!entityComponentTypes.has(expectedType)) {
|
|
893
|
-
return false;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
return true;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Sort hooks by priority (higher priority first)
|
|
902
|
-
*/
|
|
903
|
-
private sortHooksByPriority(eventType: string): void {
|
|
904
|
-
const hooks = this.hooks.get(eventType);
|
|
905
|
-
if (hooks) {
|
|
906
|
-
hooks.sort((a, b) => (b.options.priority || 0) - (a.options.priority || 0));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
208
|
/**
|
|
911
209
|
* Get the singleton instance of EntityHookManager
|
|
912
210
|
*/
|
|
@@ -919,4 +217,4 @@ class EntityHookManager {
|
|
|
919
217
|
}
|
|
920
218
|
|
|
921
219
|
// Export singleton instance
|
|
922
|
-
export default EntityHookManager.instance;
|
|
220
|
+
export default EntityHookManager.instance;
|