bunsane 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,112 +1,52 @@
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";
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 hooks: Map<string, RegisteredHook[]> = new Map();
100
- private hookCounter: number = 0;
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.generateHookId();
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.generateHookId();
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.generateHookId();
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
- let removed = false;
257
-
258
- for (const [eventType, hooks] of this.hooks.entries()) {
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
- const eventType = event.getEventType();
277
- const hooks = this.hooks.get(eventType) || [];
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
- // Record performance metrics
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
- if (events.length === 0) {
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
- if (eventType) {
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
- if (eventType) {
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
- if (eventType) {
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.hooks.clear();
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;