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
@@ -0,0 +1,439 @@
1
+ import type { LifecycleEvent } from "../events/EntityLifecycleEvents";
2
+ import { logger as MainLogger } from "../Logger";
3
+ import type { RegisteredHook, HookMetrics, RegistryState } from "./registry";
4
+ import { matchesComponentTarget } from "./guards";
5
+
6
+ const logger = MainLogger.child({ scope: "EntityHookManager" });
7
+
8
+ /**
9
+ * Dispatcher state owned by the manager instance
10
+ */
11
+ export interface DispatcherState {
12
+ metrics: Map<string, HookMetrics>;
13
+ globalMetrics: HookMetrics;
14
+ }
15
+
16
+ /**
17
+ * Create initial dispatcher state
18
+ */
19
+ export function createDispatcherState(): DispatcherState {
20
+ return {
21
+ metrics: new Map(),
22
+ globalMetrics: {
23
+ totalExecutions: 0,
24
+ totalExecutionTime: 0,
25
+ averageExecutionTime: 0,
26
+ errorCount: 0,
27
+ lastExecutionTime: 0
28
+ }
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Record hook execution metrics
34
+ */
35
+ export function recordMetrics(state: DispatcherState, eventType: string, executionTime: number, hadErrors: boolean): void {
36
+ // Update event-specific metrics
37
+ let eventMetrics = state.metrics.get(eventType);
38
+ if (!eventMetrics) {
39
+ eventMetrics = {
40
+ totalExecutions: 0,
41
+ totalExecutionTime: 0,
42
+ averageExecutionTime: 0,
43
+ errorCount: 0,
44
+ lastExecutionTime: 0
45
+ };
46
+ state.metrics.set(eventType, eventMetrics);
47
+ }
48
+
49
+ eventMetrics.totalExecutions++;
50
+ eventMetrics.totalExecutionTime += executionTime;
51
+ eventMetrics.averageExecutionTime = eventMetrics.totalExecutionTime / eventMetrics.totalExecutions;
52
+ eventMetrics.lastExecutionTime = executionTime;
53
+ if (hadErrors) {
54
+ eventMetrics.errorCount++;
55
+ }
56
+
57
+ // Update global metrics
58
+ state.globalMetrics.totalExecutions++;
59
+ state.globalMetrics.totalExecutionTime += executionTime;
60
+ state.globalMetrics.averageExecutionTime = state.globalMetrics.totalExecutionTime / state.globalMetrics.totalExecutions;
61
+ state.globalMetrics.lastExecutionTime = executionTime;
62
+ if (hadErrors) {
63
+ state.globalMetrics.errorCount++;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get performance metrics for hook execution
69
+ */
70
+ export function getMetrics(state: DispatcherState, eventType?: string): HookMetrics {
71
+ if (eventType) {
72
+ return state.metrics.get(eventType) || {
73
+ totalExecutions: 0,
74
+ totalExecutionTime: 0,
75
+ averageExecutionTime: 0,
76
+ errorCount: 0,
77
+ lastExecutionTime: 0
78
+ };
79
+ }
80
+ return { ...state.globalMetrics };
81
+ }
82
+
83
+ /**
84
+ * Reset performance metrics
85
+ */
86
+ export function resetMetrics(state: DispatcherState, eventType?: string): void {
87
+ if (eventType) {
88
+ state.metrics.delete(eventType);
89
+ } else {
90
+ state.metrics.clear();
91
+ state.globalMetrics = {
92
+ totalExecutions: 0,
93
+ totalExecutionTime: 0,
94
+ averageExecutionTime: 0,
95
+ errorCount: 0,
96
+ lastExecutionTime: 0
97
+ };
98
+ }
99
+ logger.trace(`Reset metrics${eventType ? ` for ${eventType}` : ''}`);
100
+ }
101
+
102
+ /**
103
+ * Execute hooks for a specific event
104
+ */
105
+ export async function executeHooks(registryState: RegistryState, dispatcherState: DispatcherState, event: LifecycleEvent): Promise<void> {
106
+ const eventType = event.getEventType();
107
+ const hooks = registryState.hooks.get(eventType) || [];
108
+ const startTime = performance.now();
109
+ let hadErrors = false;
110
+
111
+ if (hooks.length === 0) {
112
+ return;
113
+ }
114
+
115
+ logger.trace(`Executing ${hooks.length} hooks for event: ${eventType}`);
116
+
117
+ // Separate sync and async hooks
118
+ const syncHooks = hooks.filter(hook => !hook.options.async);
119
+ const asyncHooks = hooks.filter(hook => hook.options.async);
120
+
121
+ // Execute sync hooks immediately
122
+ for (const hook of syncHooks) {
123
+ // Check component targeting first
124
+ if (!matchesComponentTarget(event, hook.options.componentTarget)) {
125
+ continue;
126
+ }
127
+
128
+ // Check filter condition
129
+ if (hook.options.filter && !hook.options.filter(event)) {
130
+ continue;
131
+ }
132
+
133
+ try {
134
+ if (hook.options.timeout && hook.options.timeout > 0) {
135
+ // Execute with timeout. Timer handle is stored so the
136
+ // normal-completion path clears it (no leaked pending
137
+ // timers per successful hook). The underlying callback
138
+ // promise is attached with a detached .catch so a late
139
+ // rejection after timeout does not escape as unhandled
140
+ // (H-HOOK-2 / H-MEM-2).
141
+ let timerHandle: ReturnType<typeof setTimeout> | null = null;
142
+ const timeoutPromise = new Promise<never>((_, reject) => {
143
+ timerHandle = setTimeout(
144
+ () => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
145
+ hook.options.timeout
146
+ );
147
+ (timerHandle as unknown as { unref?: () => void }).unref?.();
148
+ });
149
+ const hookPromise = Promise.resolve().then(() => hook.callback(event));
150
+ hookPromise.catch((err) => {
151
+ logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
152
+ });
153
+ try {
154
+ await Promise.race([hookPromise, timeoutPromise]);
155
+ } finally {
156
+ if (timerHandle) clearTimeout(timerHandle);
157
+ }
158
+ } else {
159
+ // Always await — callback may be an async function declared
160
+ // with async:false by mistake. Without await, a rejection
161
+ // from such a callback escapes as an unhandled rejection
162
+ // and crashes the process under strict mode (C13).
163
+ await hook.callback(event);
164
+ }
165
+ } catch (error) {
166
+ logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
167
+ hadErrors = true;
168
+ // Continue executing other hooks even if one fails
169
+ }
170
+ }
171
+
172
+ // Execute async hooks in parallel
173
+ if (asyncHooks.length > 0) {
174
+ const asyncPromises = asyncHooks.map(async (hook) => {
175
+ // Check component targeting first
176
+ if (!matchesComponentTarget(event, hook.options.componentTarget)) {
177
+ return;
178
+ }
179
+
180
+ // Check filter condition
181
+ if (hook.options.filter && !hook.options.filter(event)) {
182
+ return;
183
+ }
184
+
185
+ try {
186
+ if (hook.options.timeout && hook.options.timeout > 0) {
187
+ // Execute with timeout. See sync path for rationale —
188
+ // clear the timer on normal completion and detach a
189
+ // .catch on the hook promise so late rejections do
190
+ // not escape (H-HOOK-2 / H-MEM-2).
191
+ let timerHandle: ReturnType<typeof setTimeout> | null = null;
192
+ const hookPromise = Promise.resolve().then(() => hook.callback(event));
193
+ hookPromise.catch((err) => {
194
+ logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
195
+ });
196
+ const timeoutPromise = new Promise<never>((_, reject) => {
197
+ timerHandle = setTimeout(
198
+ () => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
199
+ hook.options.timeout
200
+ );
201
+ (timerHandle as unknown as { unref?: () => void }).unref?.();
202
+ });
203
+ try {
204
+ await Promise.race([hookPromise, timeoutPromise]);
205
+ } finally {
206
+ if (timerHandle) clearTimeout(timerHandle);
207
+ }
208
+ } else {
209
+ // Execute normally
210
+ await hook.callback(event);
211
+ }
212
+ } catch (error) {
213
+ logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
214
+ hadErrors = true;
215
+ // Continue executing other hooks even if one fails
216
+ }
217
+ });
218
+
219
+ await Promise.allSettled(asyncPromises);
220
+ }
221
+
222
+ // Record performance metrics
223
+ const executionTime = performance.now() - startTime;
224
+ recordMetrics(dispatcherState, eventType, executionTime, hadErrors);
225
+ }
226
+
227
+ /**
228
+ * Execute hooks for multiple events in batch
229
+ */
230
+ export async function executeHooksBatch(registryState: RegistryState, dispatcherState: DispatcherState, events: LifecycleEvent[]): Promise<void> {
231
+ if (events.length === 0) {
232
+ return;
233
+ }
234
+
235
+ logger.trace(`Executing hooks for ${events.length} events in batch`);
236
+
237
+ // Group events by type for efficient processing
238
+ const eventsByType = new Map<string, LifecycleEvent[]>();
239
+ for (const event of events) {
240
+ const eventType = event.getEventType();
241
+ if (!eventsByType.has(eventType)) {
242
+ eventsByType.set(eventType, []);
243
+ }
244
+ eventsByType.get(eventType)!.push(event);
245
+ }
246
+
247
+ // Process each event type
248
+ const promises: Promise<void>[] = [];
249
+ for (const [eventType, typeEvents] of eventsByType.entries()) {
250
+ promises.push(executeHooksForType(registryState, dispatcherState, eventType, typeEvents));
251
+ }
252
+
253
+ await Promise.allSettled(promises);
254
+ }
255
+
256
+ /**
257
+ * Execute hooks for a specific event type with multiple events
258
+ */
259
+ async function executeHooksForType(registryState: RegistryState, dispatcherState: DispatcherState, eventType: string, events: LifecycleEvent[]): Promise<void> {
260
+ const hooks = registryState.hooks.get(eventType) || [];
261
+
262
+ if (hooks.length === 0 || events.length === 0) {
263
+ return;
264
+ }
265
+
266
+ logger.trace(`Executing ${hooks.length} hooks for ${events.length} ${eventType} events`);
267
+
268
+ // Pre-filter hooks by component targeting to avoid repeated checks
269
+ const preFilteredHooks = preFilterHooksByComponentTargeting(hooks, events);
270
+
271
+ if (preFilteredHooks.length === 0) {
272
+ return;
273
+ }
274
+
275
+ // Separate sync and async hooks
276
+ const syncHooks = preFilteredHooks.filter(hook => !hook.options.async);
277
+ const asyncHooks = preFilteredHooks.filter(hook => hook.options.async);
278
+
279
+ // Execute sync hooks for all events with batch optimization
280
+ if (syncHooks.length > 0) {
281
+ await executeSyncHooksBatch(dispatcherState, syncHooks, events, eventType);
282
+ }
283
+
284
+ // Execute async hooks in parallel for all events with batch optimization
285
+ if (asyncHooks.length > 0) {
286
+ await executeAsyncHooksBatch(dispatcherState, asyncHooks, events, eventType);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Pre-filter hooks based on component targeting to optimize batch processing
292
+ */
293
+ function preFilterHooksByComponentTargeting(hooks: RegisteredHook[], events: LifecycleEvent[]): RegisteredHook[] {
294
+ // If no hooks have component targeting, return all hooks (preserving order)
295
+ const hasComponentTargeting = hooks.some(hook => hook.options.componentTarget);
296
+ if (!hasComponentTargeting) {
297
+ return [...hooks]; // Return a copy to avoid modifying the original
298
+ }
299
+
300
+ // For hooks with component targeting, check if they could match any event
301
+ // This is a broad pre-filter to avoid checking every hook against every event
302
+ const filteredHooks = hooks.filter(hook => {
303
+ if (!hook.options.componentTarget) {
304
+ return true; // No targeting means it matches all
305
+ }
306
+
307
+ // Check if this hook could potentially match any of the events
308
+ return events.some(event => matchesComponentTarget(event, hook.options.componentTarget));
309
+ });
310
+
311
+ // Return filtered hooks in their original order (priority should already be sorted)
312
+ return filteredHooks;
313
+ }
314
+
315
+ /**
316
+ * Execute sync hooks for multiple events with batch optimizations
317
+ */
318
+ async function executeSyncHooksBatch(dispatcherState: DispatcherState, syncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
319
+ const startTime = performance.now();
320
+ let hadErrors = false;
321
+
322
+ // Execute hooks in priority order across all events to maintain deterministic execution
323
+ for (const hook of syncHooks) {
324
+ // Process all events for this hook
325
+ for (const event of events) {
326
+ // Double-check component targeting (pre-filter may have false positives)
327
+ if (!matchesComponentTarget(event, hook.options.componentTarget)) {
328
+ continue;
329
+ }
330
+
331
+ // Check filter condition
332
+ if (hook.options.filter && !hook.options.filter(event)) {
333
+ continue;
334
+ }
335
+
336
+ try {
337
+ if (hook.options.timeout && hook.options.timeout > 0) {
338
+ // Same cleanup pattern as single-event path (H-HOOK-2 / H-MEM-2).
339
+ let timerHandle: ReturnType<typeof setTimeout> | null = null;
340
+ const hookPromise = Promise.resolve().then(() => hook.callback(event));
341
+ hookPromise.catch((err) => {
342
+ logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
343
+ });
344
+ const timeoutPromise = new Promise<never>((_, reject) => {
345
+ timerHandle = setTimeout(
346
+ () => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
347
+ hook.options.timeout
348
+ );
349
+ (timerHandle as unknown as { unref?: () => void }).unref?.();
350
+ });
351
+ try {
352
+ await Promise.race([hookPromise, timeoutPromise]);
353
+ } finally {
354
+ if (timerHandle) clearTimeout(timerHandle);
355
+ }
356
+ } else {
357
+ // Await so async callbacks do not escape as unhandled
358
+ // rejections (C13 parity).
359
+ await hook.callback(event);
360
+ }
361
+ } catch (error) {
362
+ logger.error(`Error executing sync hook ${hook.id} for event ${eventType}: ${error}`);
363
+ hadErrors = true;
364
+ }
365
+ }
366
+ }
367
+
368
+ // Record performance metrics
369
+ const executionTime = performance.now() - startTime;
370
+ recordMetrics(dispatcherState, eventType, executionTime, hadErrors);
371
+ }
372
+
373
+ /**
374
+ * Execute async hooks for multiple events with batch optimizations
375
+ */
376
+ async function executeAsyncHooksBatch(dispatcherState: DispatcherState, asyncHooks: RegisteredHook[], events: LifecycleEvent[], eventType: string): Promise<void> {
377
+ const startTime = performance.now();
378
+ let hadErrors = false;
379
+
380
+ // Collect all async hook executions
381
+ const asyncPromises: Promise<void>[] = [];
382
+
383
+ // Use a more efficient batching strategy for async hooks
384
+ for (const event of events) {
385
+ for (const hook of asyncHooks) {
386
+ // Double-check component targeting
387
+ if (!matchesComponentTarget(event, hook.options.componentTarget)) {
388
+ continue;
389
+ }
390
+
391
+ // Check filter condition
392
+ if (hook.options.filter && !hook.options.filter(event)) {
393
+ continue;
394
+ }
395
+
396
+ asyncPromises.push(
397
+ (async () => {
398
+ try {
399
+ if (hook.options.timeout && hook.options.timeout > 0) {
400
+ // Same cleanup pattern (H-HOOK-2 / H-MEM-2).
401
+ let timerHandle: ReturnType<typeof setTimeout> | null = null;
402
+ const hookPromise = Promise.resolve().then(() => hook.callback(event));
403
+ hookPromise.catch((err) => {
404
+ logger.warn({ hookId: hook.id, err }, `Late rejection from hook after timeout`);
405
+ });
406
+ const timeoutPromise = new Promise<never>((_, reject) => {
407
+ timerHandle = setTimeout(
408
+ () => reject(new Error(`Hook ${hook.id} timed out after ${hook.options.timeout}ms`)),
409
+ hook.options.timeout
410
+ );
411
+ (timerHandle as unknown as { unref?: () => void }).unref?.();
412
+ });
413
+ try {
414
+ await Promise.race([hookPromise, timeoutPromise]);
415
+ } finally {
416
+ if (timerHandle) clearTimeout(timerHandle);
417
+ }
418
+ } else {
419
+ // Execute normally
420
+ await hook.callback(event);
421
+ }
422
+ } catch (error) {
423
+ logger.error(`Error executing async hook ${hook.id} for event ${eventType}: ${error}`);
424
+ hadErrors = true;
425
+ }
426
+ })()
427
+ );
428
+ }
429
+ }
430
+
431
+ // Execute all async hooks in parallel with controlled concurrency
432
+ if (asyncPromises.length > 0) {
433
+ await Promise.allSettled(asyncPromises);
434
+ }
435
+
436
+ // Record performance metrics
437
+ const executionTime = performance.now() - startTime;
438
+ recordMetrics(dispatcherState, eventType, executionTime, hadErrors);
439
+ }
@@ -0,0 +1,155 @@
1
+ import type { BaseComponent } from "../components";
2
+ import type ArcheType from "../ArcheType";
3
+ import type { LifecycleEvent } from "../events/EntityLifecycleEvents";
4
+ import type { ComponentTargetConfig } from "./registry";
5
+ import { typeIdOfCtor } from "./registry";
6
+
7
+ /**
8
+ * Check if an event matches the component targeting configuration
9
+ */
10
+ export function matchesComponentTarget(event: LifecycleEvent, componentTarget?: ComponentTargetConfig): boolean {
11
+ // If no component targeting is specified, always match
12
+ if (!componentTarget) {
13
+ return true;
14
+ }
15
+
16
+ const entity = event.getEntity();
17
+ const entityComponents = entity.componentList();
18
+
19
+ // Check archetype matching first (most specific)
20
+ if (componentTarget.archetype) {
21
+ if (!matchesArchetype(entityComponents, componentTarget.archetype, !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length))) {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ // Check multiple archetypes (OR logic)
27
+ if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
28
+ const allowExtra = !!(componentTarget.includeComponents?.length || componentTarget.excludeComponents?.length);
29
+ const matchesAnyArchetype = componentTarget.archetypes.some(archetype =>
30
+ matchesArchetype(entityComponents, archetype, allowExtra)
31
+ );
32
+ if (!matchesAnyArchetype) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ // Check included components
38
+ if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
39
+ const includeMatch = checkComponentPresence(
40
+ entityComponents,
41
+ componentTarget.includeComponents,
42
+ componentTarget.requireAllIncluded ?? true
43
+ );
44
+
45
+ if (!includeMatch) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // Check excluded components
51
+ if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
52
+ const excludeMatch = checkComponentAbsence(
53
+ entityComponents,
54
+ componentTarget.excludeComponents,
55
+ componentTarget.requireAllExcluded ?? true
56
+ );
57
+
58
+ if (!excludeMatch) {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Check if required components are present on the entity
68
+ */
69
+ export function checkComponentPresence(
70
+ entityComponents: BaseComponent[],
71
+ requiredComponents: (new () => BaseComponent)[],
72
+ requireAll: boolean
73
+ ): boolean {
74
+ const entityComponentTypes = new Set(
75
+ entityComponents.map(comp => comp.getTypeID())
76
+ );
77
+
78
+ const requiredTypeIds = requiredComponents.map(typeIdOfCtor);
79
+
80
+ if (requireAll) {
81
+ // ALL required components must be present (AND logic)
82
+ return requiredTypeIds.every(typeId => entityComponentTypes.has(typeId));
83
+ } else {
84
+ // ANY required component must be present (OR logic)
85
+ return requiredTypeIds.some(typeId => entityComponentTypes.has(typeId));
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Check if excluded components are absent from the entity
91
+ */
92
+ export function checkComponentAbsence(
93
+ entityComponents: BaseComponent[],
94
+ excludedComponents: (new () => BaseComponent)[],
95
+ requireAll: boolean
96
+ ): boolean {
97
+ const entityComponentTypes = new Set(
98
+ entityComponents.map(comp => comp.getTypeID())
99
+ );
100
+
101
+ const excludedTypeIds = excludedComponents.map(typeIdOfCtor);
102
+
103
+ if (requireAll) {
104
+ // ALL excluded components must be absent (AND logic)
105
+ return excludedTypeIds.every(typeId => !entityComponentTypes.has(typeId));
106
+ } else {
107
+ // ANY excluded component must be absent (OR logic) - this is less common but supported
108
+ return excludedTypeIds.some(typeId => !entityComponentTypes.has(typeId));
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check if entity components match a specific archetype
114
+ */
115
+ export function matchesArchetype(entityComponents: BaseComponent[], archetype: ArcheType, allowExtraComponents: boolean = false): boolean {
116
+ // Get the expected component types from the archetype
117
+ // We need to access the private componentMap from ArcheType
118
+ const archetypeComponentMap = (archetype as any).componentMap as Record<string, typeof BaseComponent>;
119
+
120
+ if (!archetypeComponentMap) {
121
+ return false;
122
+ }
123
+
124
+ const expectedComponentTypes = new Set(
125
+ Object.values(archetypeComponentMap).map(compCtor => typeIdOfCtor(compCtor as any))
126
+ );
127
+
128
+ const entityComponentTypes = new Set(
129
+ entityComponents.map(comp => comp.getTypeID())
130
+ );
131
+
132
+ if (allowExtraComponents) {
133
+ // Entity must have at least all the component types from the archetype
134
+ // (allows additional components beyond the archetype)
135
+ for (const expectedType of expectedComponentTypes) {
136
+ if (!entityComponentTypes.has(expectedType)) {
137
+ return false;
138
+ }
139
+ }
140
+ return true;
141
+ } else {
142
+ // Entity must have exactly the same component types as the archetype
143
+ if (expectedComponentTypes.size !== entityComponentTypes.size) {
144
+ return false;
145
+ }
146
+
147
+ // All expected component types must be present in the entity
148
+ for (const expectedType of expectedComponentTypes) {
149
+ if (!entityComponentTypes.has(expectedType)) {
150
+ return false;
151
+ }
152
+ }
153
+ return true;
154
+ }
155
+ }