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