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