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.
- package/CHANGELOG.md +445 -318
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1043
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +64 -6
- package/core/RequestLoaders.ts +187 -36
- package/core/SchedulerManager.ts +28 -600
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +85 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +15 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +310 -0
- package/core/app/restRegistry.ts +80 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +666 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +161 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +173 -267
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +4 -4
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +16 -8
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +364 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +377 -0
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/AccessLog.ts +8 -1
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +20 -5
- package/database/cancellable.ts +35 -0
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -0
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +240 -13
- package/query/OrNode.ts +6 -5
- package/query/Query.ts +203 -59
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -367
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- package/tests/utils/test-context.ts +0 -149
|
@@ -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
|
+
}
|