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,420 @@
|
|
|
1
|
+
import { logger } from "../Logger";
|
|
2
|
+
import { Query } from "../../query/Query";
|
|
3
|
+
import ArcheType from "../ArcheType";
|
|
4
|
+
import { BaseComponent } from "../components";
|
|
5
|
+
import type { ScheduledTaskInfo, SchedulerMetrics, TaskMetrics } from "../../types/scheduler.types";
|
|
6
|
+
import type { ComponentTargetConfig } from "../EntityHookManager";
|
|
7
|
+
import type { SchedulerManager } from "../SchedulerManager";
|
|
8
|
+
|
|
9
|
+
const loggerInstance = logger.child({ scope: "SchedulerManager" });
|
|
10
|
+
|
|
11
|
+
export function updateTaskMetrics(manager: SchedulerManager, taskId: string, updates: Partial<TaskMetrics>): void {
|
|
12
|
+
if (!manager.metrics.taskMetrics[taskId]) {
|
|
13
|
+
const taskInfo = manager.tasks.get(taskId);
|
|
14
|
+
manager.metrics.taskMetrics[taskId] = {
|
|
15
|
+
taskId,
|
|
16
|
+
taskName: taskInfo?.name || 'Unknown',
|
|
17
|
+
totalExecutions: 0,
|
|
18
|
+
successfulExecutions: 0,
|
|
19
|
+
failedExecutions: 0,
|
|
20
|
+
averageExecutionTime: 0,
|
|
21
|
+
totalEntitiesProcessed: 0,
|
|
22
|
+
retryCount: 0,
|
|
23
|
+
timeoutCount: 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metrics = manager.metrics.taskMetrics[taskId];
|
|
28
|
+
Object.assign(metrics, updates);
|
|
29
|
+
|
|
30
|
+
// Update rolling averages
|
|
31
|
+
if (updates.averageExecutionTime !== undefined) {
|
|
32
|
+
const currentAvg = metrics.averageExecutionTime;
|
|
33
|
+
const newCount = metrics.totalExecutions;
|
|
34
|
+
metrics.averageExecutionTime = ((currentAvg * (newCount - 1)) + updates.averageExecutionTime) / newCount;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute a task with timeout enforcement.
|
|
40
|
+
*
|
|
41
|
+
* Note: JS has no way to cancel an arbitrary Promise — on timeout the
|
|
42
|
+
* wrapper rejects but the underlying task continues. The second .catch
|
|
43
|
+
* below captures a late rejection after the wrapper already rejected,
|
|
44
|
+
* preventing an unhandled-rejection process crash (H-SCHED-5). The
|
|
45
|
+
* `settled` flag guards against double-settle.
|
|
46
|
+
*/
|
|
47
|
+
export async function executeWithTimeout<T>(
|
|
48
|
+
manager: SchedulerManager,
|
|
49
|
+
task: Promise<T>,
|
|
50
|
+
timeoutMs: number,
|
|
51
|
+
taskInfo: ScheduledTaskInfo
|
|
52
|
+
): Promise<T> {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let settled = false;
|
|
55
|
+
const timeoutId = setTimeout(() => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
manager.metrics.timedOutTasks++;
|
|
59
|
+
updateTaskMetrics(manager, taskInfo.id, {
|
|
60
|
+
timeoutCount: (manager.metrics.taskMetrics[taskInfo.id]?.timeoutCount || 0) + 1
|
|
61
|
+
});
|
|
62
|
+
const error = new Error(`Task ${taskInfo.name} timed out after ${timeoutMs}ms`);
|
|
63
|
+
manager.emitEvent({
|
|
64
|
+
type: 'task.timeout',
|
|
65
|
+
taskId: taskInfo.id,
|
|
66
|
+
timestamp: new Date(),
|
|
67
|
+
data: { timeoutMs, taskName: taskInfo.name }
|
|
68
|
+
});
|
|
69
|
+
reject(error);
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
|
|
72
|
+
task
|
|
73
|
+
.then((result) => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
resolve(result);
|
|
78
|
+
})
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
if (settled) {
|
|
81
|
+
// Late rejection after timeout. Log only — the wrapper
|
|
82
|
+
// promise is already settled, so re-rejecting would be
|
|
83
|
+
// a no-op but the rejection would escape as unhandled.
|
|
84
|
+
loggerInstance.warn({ taskId: taskInfo.id, err: error }, 'Late rejection from scheduled task after timeout');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
reject(error);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle task failure with retry logic
|
|
96
|
+
*/
|
|
97
|
+
export async function handleTaskFailure(
|
|
98
|
+
manager: SchedulerManager,
|
|
99
|
+
taskInfo: ScheduledTaskInfo,
|
|
100
|
+
error: Error,
|
|
101
|
+
duration: number
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
taskInfo.lastError = error.message;
|
|
104
|
+
|
|
105
|
+
const maxRetries = taskInfo.options?.maxRetries || taskInfo.maxRetries || 0;
|
|
106
|
+
const retryDelay = taskInfo.options?.retryDelay || 1000; // Default 1 second
|
|
107
|
+
|
|
108
|
+
if (taskInfo.retryCount === undefined) {
|
|
109
|
+
taskInfo.retryCount = 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (taskInfo.retryCount < maxRetries) {
|
|
113
|
+
taskInfo.retryCount++;
|
|
114
|
+
manager.metrics.retriedTasks++;
|
|
115
|
+
|
|
116
|
+
updateTaskMetrics(manager, taskInfo.id, {
|
|
117
|
+
retryCount: taskInfo.retryCount
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (manager.config.enableLogging) {
|
|
121
|
+
loggerInstance.warn(`Task ${taskInfo.name} failed (attempt ${taskInfo.retryCount}/${maxRetries}), retrying in ${retryDelay}ms: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Schedule retry. Track the timer handle in `intervals` under a
|
|
125
|
+
// unique key so `stop()` can clear it (H-SCHED-3); without this
|
|
126
|
+
// the retry fires post-shutdown against a closed DB pool. Also
|
|
127
|
+
// skip the retry if the scheduler is no longer running by the
|
|
128
|
+
// time the timer fires, and rely on the new isRunning guard in
|
|
129
|
+
// doExecuteTask (H-SCHED-1) to prevent retry/tick overlap.
|
|
130
|
+
const retryKey = `${taskInfo.id}:retry:${taskInfo.retryCount}`;
|
|
131
|
+
const retryHandle = setTimeout(async () => {
|
|
132
|
+
manager.intervals.delete(retryKey);
|
|
133
|
+
if (!manager.isRunning) return;
|
|
134
|
+
await (manager as any).executeTask(taskInfo.id);
|
|
135
|
+
}, retryDelay);
|
|
136
|
+
manager.intervals.set(retryKey, retryHandle as any);
|
|
137
|
+
|
|
138
|
+
manager.emitEvent({
|
|
139
|
+
type: 'task.retry',
|
|
140
|
+
taskId: taskInfo.id,
|
|
141
|
+
timestamp: new Date(),
|
|
142
|
+
data: {
|
|
143
|
+
attempt: taskInfo.retryCount,
|
|
144
|
+
maxRetries,
|
|
145
|
+
retryDelay,
|
|
146
|
+
error: error.message
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
// Max retries reached or no retries configured
|
|
151
|
+
updateTaskMetrics(manager, taskInfo.id, {
|
|
152
|
+
failedExecutions: (manager.metrics.taskMetrics[taskInfo.id]?.failedExecutions || 0) + 1
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (manager.config.enableLogging) {
|
|
156
|
+
loggerInstance.error(`Task ${taskInfo.name} failed permanently after ${taskInfo.retryCount} attempts: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
manager.emitEvent({
|
|
160
|
+
type: 'task.failed',
|
|
161
|
+
taskId: taskInfo.id,
|
|
162
|
+
timestamp: new Date(),
|
|
163
|
+
data: {
|
|
164
|
+
duration,
|
|
165
|
+
error: error.message,
|
|
166
|
+
attempts: taskInfo.retryCount,
|
|
167
|
+
maxRetries
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build a Query object from ComponentTargetConfig
|
|
175
|
+
* @param componentTarget The component targeting configuration
|
|
176
|
+
* @returns A Query object configured with the component targeting
|
|
177
|
+
*/
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
export function buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query<any> {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
let query: Query<any> = new Query();
|
|
182
|
+
|
|
183
|
+
// Handle archetype matching first (most specific)
|
|
184
|
+
if (componentTarget.archetype) {
|
|
185
|
+
// For archetype matching, we need to include all components from the archetype
|
|
186
|
+
const archetypeComponents = getArchetypeComponents(componentTarget.archetype);
|
|
187
|
+
for (const component of archetypeComponents) {
|
|
188
|
+
query = query.with(component);
|
|
189
|
+
}
|
|
190
|
+
} else if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
|
|
191
|
+
// Handle multiple archetypes - for simplicity, we'll use the first valid one
|
|
192
|
+
// In a more advanced implementation, you might want to handle OR logic
|
|
193
|
+
const firstArchetype = componentTarget.archetypes.find(archetype => archetype !== undefined);
|
|
194
|
+
if (firstArchetype) {
|
|
195
|
+
const archetypeComponents = getArchetypeComponents(firstArchetype);
|
|
196
|
+
for (const component of archetypeComponents) {
|
|
197
|
+
query = query.with(component);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Handle included components
|
|
203
|
+
if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
|
|
204
|
+
const requireAll = componentTarget.requireAllIncluded ?? true;
|
|
205
|
+
if (requireAll) {
|
|
206
|
+
// ALL included components must be present (AND logic)
|
|
207
|
+
for (const component of componentTarget.includeComponents) {
|
|
208
|
+
query = query.with(component);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// ANY included component must be present (OR logic)
|
|
212
|
+
// For OR logic with Query API, we need to use a different approach
|
|
213
|
+
// This is a simplified implementation - in practice, you might need custom query logic
|
|
214
|
+
for (const component of componentTarget.includeComponents) {
|
|
215
|
+
query = query.with(component);
|
|
216
|
+
break; // Just use the first one for simplicity
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle excluded components
|
|
222
|
+
if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
|
|
223
|
+
for(const component of componentTarget.excludeComponents){
|
|
224
|
+
query = query.without(component);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return query;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract component classes from an ArcheType
|
|
233
|
+
* @param archetype The archetype to extract components from
|
|
234
|
+
* @returns Array of component classes
|
|
235
|
+
*/
|
|
236
|
+
export function getArchetypeComponents(archetype: ArcheType): (new () => BaseComponent)[] {
|
|
237
|
+
// Access the private componentMap from ArcheType
|
|
238
|
+
const componentMap = (archetype as any).componentMap as Record<string, new () => BaseComponent>;
|
|
239
|
+
if (!componentMap) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
return Object.values(componentMap);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function doExecuteTask(manager: SchedulerManager, taskId: string): Promise<void> {
|
|
246
|
+
const taskInfo = manager.tasks.get(taskId);
|
|
247
|
+
if (!taskInfo || !taskInfo.enabled) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Skip if the previous tick is still executing. Without this guard
|
|
252
|
+
// a slow task with interval < execution-time burns a lock-acquire
|
|
253
|
+
// round-trip every tick and floods the skipped-executions metric
|
|
254
|
+
// (H-SCHED-1). Cheap in-process check before reaching out to PG.
|
|
255
|
+
if (taskInfo.isRunning) {
|
|
256
|
+
manager.metrics.skippedExecutions++;
|
|
257
|
+
if (manager.config.enableLogging) {
|
|
258
|
+
loggerInstance.debug(`Task ${taskInfo.name} skipped - previous execution still running`);
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (manager.metrics.runningTasks >= manager.config.maxConcurrentTasks) {
|
|
264
|
+
if (manager.config.enableLogging) {
|
|
265
|
+
loggerInstance.warn(`Maximum concurrent tasks reached. Skipping execution of ${taskInfo.name}`);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Try to acquire distributed lock before executing
|
|
271
|
+
manager.metrics.lockAttempts++;
|
|
272
|
+
const lockResult = await manager.distributedLock.tryAcquire(taskId);
|
|
273
|
+
|
|
274
|
+
if (!lockResult.acquired) {
|
|
275
|
+
// Another instance is executing this task
|
|
276
|
+
manager.metrics.skippedExecutions++;
|
|
277
|
+
|
|
278
|
+
if (manager.config.enableLogging) {
|
|
279
|
+
loggerInstance.debug(`Task ${taskInfo.name} skipped - another instance is executing (lock key: ${lockResult.lockKey})`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
manager.emitEvent({
|
|
283
|
+
type: 'task.skipped',
|
|
284
|
+
taskId: taskInfo.id,
|
|
285
|
+
timestamp: new Date(),
|
|
286
|
+
data: { reason: 'lock_unavailable', lockKey: lockResult.lockKey.toString() }
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Lock acquired successfully
|
|
293
|
+
manager.metrics.locksAcquired++;
|
|
294
|
+
|
|
295
|
+
manager.emitEvent({
|
|
296
|
+
type: 'task.lock.acquired',
|
|
297
|
+
taskId: taskInfo.id,
|
|
298
|
+
timestamp: new Date(),
|
|
299
|
+
data: { lockKey: lockResult.lockKey.toString() }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
taskInfo.isRunning = true;
|
|
303
|
+
taskInfo.lastExecution = new Date();
|
|
304
|
+
manager.metrics.runningTasks++;
|
|
305
|
+
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
const timeout = taskInfo.options?.timeout || manager.config.defaultTimeout;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
// Create query based on targeting configuration
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
312
|
+
let query: Query<any> | null = null;
|
|
313
|
+
|
|
314
|
+
if (taskInfo.options?.query) {
|
|
315
|
+
// Use custom query function (preferred approach)
|
|
316
|
+
query = taskInfo.options.query();
|
|
317
|
+
} else if (taskInfo.options?.componentTarget) {
|
|
318
|
+
// Use component targeting configuration (deprecated - use query instead)
|
|
319
|
+
const componentTarget = taskInfo.options.componentTarget;
|
|
320
|
+
query = buildQueryFromComponentTarget(componentTarget);
|
|
321
|
+
} else if (taskInfo.componentTarget) {
|
|
322
|
+
// Use legacy single component targeting (deprecated - use query instead)
|
|
323
|
+
query = new Query().with(taskInfo.componentTarget);
|
|
324
|
+
}
|
|
325
|
+
// else: time-based task — no entity selection. Handler invoked
|
|
326
|
+
// with no arguments on each tick.
|
|
327
|
+
|
|
328
|
+
// Apply entity limit if specified (can be used with query function)
|
|
329
|
+
if (query && taskInfo.options?.maxEntitiesPerExecution) {
|
|
330
|
+
query.take(taskInfo.options.maxEntitiesPerExecution);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const entities = query ? await query.exec() : [];
|
|
334
|
+
|
|
335
|
+
// Execute the scheduled method with the entities array
|
|
336
|
+
const method = taskInfo.service[taskInfo.methodName];
|
|
337
|
+
if (typeof method !== 'function') {
|
|
338
|
+
throw new Error(`Method ${taskInfo.methodName} not found on service`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Execute with timeout. Time-based tasks receive no entity arg.
|
|
342
|
+
const result = await executeWithTimeout(
|
|
343
|
+
manager,
|
|
344
|
+
query
|
|
345
|
+
? method.call(taskInfo.service, entities)
|
|
346
|
+
: method.call(taskInfo.service),
|
|
347
|
+
timeout,
|
|
348
|
+
taskInfo
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const duration = Date.now() - startTime;
|
|
352
|
+
taskInfo.executionCount++;
|
|
353
|
+
manager.metrics.completedExecutions++;
|
|
354
|
+
manager.metrics.totalExecutionTime += duration;
|
|
355
|
+
manager.metrics.averageExecutionTime = manager.metrics.totalExecutionTime / manager.metrics.completedExecutions;
|
|
356
|
+
|
|
357
|
+
// Update task-specific metrics
|
|
358
|
+
updateTaskMetrics(manager, taskInfo.id, {
|
|
359
|
+
totalExecutions: taskInfo.executionCount,
|
|
360
|
+
successfulExecutions: (manager.metrics.taskMetrics[taskInfo.id]?.successfulExecutions || 0) + 1,
|
|
361
|
+
averageExecutionTime: duration,
|
|
362
|
+
lastExecutionTime: new Date(),
|
|
363
|
+
totalEntitiesProcessed: entities.length
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (manager.config.enableLogging) {
|
|
367
|
+
loggerInstance.info(`Task ${taskInfo.name} completed successfully in ${duration}ms (processed ${entities.length} entities)`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
manager.emitEvent({
|
|
371
|
+
type: 'task.executed',
|
|
372
|
+
taskId: taskInfo.id,
|
|
373
|
+
timestamp: new Date(),
|
|
374
|
+
data: { duration, entitiesProcessed: entities.length, success: true }
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
} catch (error) {
|
|
378
|
+
const duration = Date.now() - startTime;
|
|
379
|
+
manager.metrics.failedExecutions++;
|
|
380
|
+
|
|
381
|
+
// Handle retry logic
|
|
382
|
+
await handleTaskFailure(manager, taskInfo, error instanceof Error ? error : new Error(String(error)), duration);
|
|
383
|
+
|
|
384
|
+
if (manager.config.enableLogging) {
|
|
385
|
+
loggerInstance.error(`Task ${taskInfo.name} failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
manager.emitEvent({
|
|
389
|
+
type: 'task.failed',
|
|
390
|
+
taskId: taskInfo.id,
|
|
391
|
+
timestamp: new Date(),
|
|
392
|
+
data: { duration, error: error instanceof Error ? error.message : String(error) }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
} finally {
|
|
396
|
+
taskInfo.isRunning = false;
|
|
397
|
+
manager.metrics.runningTasks--;
|
|
398
|
+
|
|
399
|
+
// Release the distributed lock
|
|
400
|
+
await manager.distributedLock.release(taskId);
|
|
401
|
+
|
|
402
|
+
manager.emitEvent({
|
|
403
|
+
type: 'task.lock.released',
|
|
404
|
+
taskId: taskInfo.id,
|
|
405
|
+
timestamp: new Date(),
|
|
406
|
+
data: { lockKey: lockResult.lockKey.toString() }
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function executeTask(manager: SchedulerManager, taskId: string): Promise<void> {
|
|
412
|
+
// Track this execution so stop() can await in-flight work before
|
|
413
|
+
// resources (DB pool, cache) are torn down. Without this, a task mid-
|
|
414
|
+
// write during SIGTERM hits a closed DB pool and silently corrupts
|
|
415
|
+
// or loses data.
|
|
416
|
+
const p = doExecuteTask(manager, taskId);
|
|
417
|
+
manager.inflightTasks.add(p);
|
|
418
|
+
p.finally(() => manager.inflightTasks.delete(p));
|
|
419
|
+
return p;
|
|
420
|
+
}
|