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
package/core/SchedulerManager.ts
CHANGED
|
@@ -19,20 +19,25 @@ import type { ComponentTargetConfig } from "./EntityHookManager";
|
|
|
19
19
|
import ArcheType from "./ArcheType";
|
|
20
20
|
import { BaseComponent } from "./components";
|
|
21
21
|
import { DistributedLock, type DistributedLockConfig } from "./scheduler/DistributedLock";
|
|
22
|
+
import { scheduleTask, scheduleJob } from "./scheduler/cronEvaluator";
|
|
23
|
+
import { executeTask, doExecuteTask, updateTaskMetrics } from "./scheduler/taskRunner";
|
|
24
|
+
import { getDistributedLockInfo, isDistributedLockingEnabled, syncLockConfig } from "./scheduler/lockCoordinator";
|
|
25
|
+
import { initializeLifecycleIntegration, disposeLifecycleIntegration as _disposeLifecycleIntegration } from "./scheduler/lifecycleHooks";
|
|
26
|
+
import { getMetrics, getTaskMetrics, getAllTaskMetrics } from "./scheduler/metrics";
|
|
22
27
|
|
|
23
28
|
const loggerInstance = logger.child({ scope: "SchedulerManager" });
|
|
24
29
|
|
|
25
30
|
export class SchedulerManager {
|
|
26
31
|
private static instance: SchedulerManager;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
public tasks: Map<string, ScheduledTaskInfo> = new Map();
|
|
33
|
+
public intervals: Map<string, NodeJS.Timeout> = new Map();
|
|
34
|
+
public isRunning: boolean = false;
|
|
30
35
|
private eventListeners: SchedulerEventCallback[] = [];
|
|
31
36
|
public config: SchedulerConfig;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
public distributedLock: DistributedLock;
|
|
38
|
+
public phaseListener: ((event: PhaseChangeEvent) => void) | null = null;
|
|
39
|
+
public inflightTasks: Set<Promise<any>> = new Set();
|
|
40
|
+
public metrics: SchedulerMetrics = {
|
|
36
41
|
totalTasks: 0,
|
|
37
42
|
runningTasks: 0,
|
|
38
43
|
completedExecutions: 0,
|
|
@@ -67,7 +72,7 @@ export class SchedulerManager {
|
|
|
67
72
|
retryInterval: this.config.lockRetryInterval ?? 100,
|
|
68
73
|
});
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
initializeLifecycleIntegration(this);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
public static getInstance(): SchedulerManager {
|
|
@@ -77,23 +82,8 @@ export class SchedulerManager {
|
|
|
77
82
|
return SchedulerManager.instance;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
private initializeLifecycleIntegration(): void {
|
|
81
|
-
this.phaseListener = (event) => {
|
|
82
|
-
const phase = event.detail;
|
|
83
|
-
if (phase === ApplicationPhase.APPLICATION_READY) {
|
|
84
|
-
if (this.config.runOnStart) {
|
|
85
|
-
this.start();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
ApplicationLifecycle.addPhaseListener(this.phaseListener);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
85
|
public disposeLifecycleIntegration(): void {
|
|
93
|
-
|
|
94
|
-
ApplicationLifecycle.removePhaseListener(this.phaseListener);
|
|
95
|
-
this.phaseListener = null;
|
|
96
|
-
}
|
|
86
|
+
_disposeLifecycleIntegration(this);
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
public registerTask(taskInfo: ScheduledTaskInfo): void {
|
|
@@ -128,7 +118,7 @@ export class SchedulerManager {
|
|
|
128
118
|
|
|
129
119
|
// Try to schedule the task - if scheduling fails, don't register it
|
|
130
120
|
try {
|
|
131
|
-
|
|
121
|
+
scheduleTask(this, taskInfo);
|
|
132
122
|
this.tasks.set(taskInfo.id, taskInfo);
|
|
133
123
|
this.metrics.totalTasks++;
|
|
134
124
|
|
|
@@ -154,341 +144,15 @@ export class SchedulerManager {
|
|
|
154
144
|
* entity-component system integration.
|
|
155
145
|
*/
|
|
156
146
|
public scheduleJob(name: string, cronExpression: string, callback: () => Promise<void> | void): { cancel: () => void } {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Validate cron expression
|
|
160
|
-
const validation = CronParser.validate(cronExpression);
|
|
161
|
-
if (!validation.isValid) {
|
|
162
|
-
throw new Error(`Invalid cron expression for job "${name}": ${validation.error}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
166
|
-
let cancelled = false;
|
|
167
|
-
|
|
168
|
-
const scheduleNextExecution = () => {
|
|
169
|
-
if (cancelled) return;
|
|
170
|
-
|
|
171
|
-
const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
|
|
172
|
-
if (!nextExecution) {
|
|
173
|
-
loggerInstance.warn(`Unable to calculate next execution for job "${name}"`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const delay = nextExecution.getTime() - Date.now();
|
|
178
|
-
timeoutId = setTimeout(async () => {
|
|
179
|
-
if (cancelled) return;
|
|
180
|
-
try {
|
|
181
|
-
await callback();
|
|
182
|
-
} catch (error) {
|
|
183
|
-
loggerInstance.error(`Job "${name}" failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
-
}
|
|
185
|
-
scheduleNextExecution();
|
|
186
|
-
}, delay);
|
|
187
|
-
|
|
188
|
-
this.intervals.set(jobId, timeoutId as any);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
scheduleNextExecution();
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
cancel: () => {
|
|
195
|
-
cancelled = true;
|
|
196
|
-
if (timeoutId) {
|
|
197
|
-
clearTimeout(timeoutId);
|
|
198
|
-
this.intervals.delete(jobId);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private scheduleTask(taskInfo: ScheduledTaskInfo): void {
|
|
205
|
-
try {
|
|
206
|
-
if (taskInfo.interval === ScheduleInterval.CRON) {
|
|
207
|
-
this.scheduleCronTask(taskInfo);
|
|
208
|
-
} else {
|
|
209
|
-
this.scheduleIntervalTask(taskInfo);
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
loggerInstance.error(`Failed to schedule task ${taskInfo.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private scheduleIntervalTask(taskInfo: ScheduledTaskInfo): void {
|
|
218
|
-
const intervalMs = this.getIntervalMilliseconds(taskInfo.interval);
|
|
219
|
-
|
|
220
|
-
// Clear any existing interval for this task before creating a new one
|
|
221
|
-
const existingInterval = this.intervals.get(taskInfo.id);
|
|
222
|
-
if (existingInterval) {
|
|
223
|
-
clearInterval(existingInterval);
|
|
224
|
-
this.intervals.delete(taskInfo.id);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// For very long intervals (monthly), use a different approach
|
|
228
|
-
if (intervalMs > 24 * 60 * 60 * 1000) { // More than 24 hours
|
|
229
|
-
this.scheduleLongIntervalTask(taskInfo, intervalMs);
|
|
230
|
-
} else {
|
|
231
|
-
const intervalId = setInterval(async () => {
|
|
232
|
-
await this.executeTask(taskInfo.id);
|
|
233
|
-
}, intervalMs);
|
|
234
|
-
|
|
235
|
-
this.intervals.set(taskInfo.id, intervalId);
|
|
236
|
-
taskInfo.nextExecution = new Date(Date.now() + intervalMs);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (this.config.enableLogging) {
|
|
240
|
-
loggerInstance.info(`Scheduled task ${taskInfo.name} to run every ${intervalMs}ms`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private scheduleLongIntervalTask(taskInfo: ScheduledTaskInfo, intervalMs: number): void {
|
|
245
|
-
// For very long intervals, use a shorter check interval to avoid timeout overflow
|
|
246
|
-
const checkInterval = Math.min(intervalMs, 24 * 60 * 60 * 1000); // Max 24 hours check interval
|
|
247
|
-
const nextExecution = new Date(Date.now() + intervalMs);
|
|
248
|
-
taskInfo.nextExecution = nextExecution;
|
|
249
|
-
|
|
250
|
-
const intervalId = setInterval(async () => {
|
|
251
|
-
const now = Date.now();
|
|
252
|
-
if (now >= nextExecution.getTime()) {
|
|
253
|
-
await this.executeTask(taskInfo.id);
|
|
254
|
-
// Reschedule for next execution
|
|
255
|
-
taskInfo.nextExecution = new Date(now + intervalMs);
|
|
256
|
-
}
|
|
257
|
-
}, checkInterval);
|
|
258
|
-
|
|
259
|
-
this.intervals.set(taskInfo.id, intervalId);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private scheduleCronTask(taskInfo: ScheduledTaskInfo): void {
|
|
263
|
-
if (!taskInfo.cronExpression) {
|
|
264
|
-
throw new Error(`Cron expression is required for CRON interval tasks`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Validate cron expression
|
|
268
|
-
const validation = CronParser.validate(taskInfo.cronExpression);
|
|
269
|
-
if (!validation.isValid) {
|
|
270
|
-
throw new Error(`Invalid cron expression: ${validation.error}`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Calculate next execution time
|
|
274
|
-
const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
|
|
275
|
-
if (!nextExecution) {
|
|
276
|
-
throw new Error(`Unable to calculate next execution time for cron expression: ${taskInfo.cronExpression}`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
taskInfo.nextExecution = nextExecution;
|
|
280
|
-
|
|
281
|
-
// Clear any existing timeout for this task before creating a new one
|
|
282
|
-
const existingTimeout = this.intervals.get(taskInfo.id);
|
|
283
|
-
if (existingTimeout) {
|
|
284
|
-
clearTimeout(existingTimeout as any);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Schedule the task to run at the calculated time
|
|
288
|
-
const timeoutId = setTimeout(async () => {
|
|
289
|
-
await this.executeTask(taskInfo.id);
|
|
290
|
-
// Reschedule for next execution
|
|
291
|
-
this.scheduleCronTask(taskInfo);
|
|
292
|
-
}, nextExecution.getTime() - Date.now());
|
|
293
|
-
|
|
294
|
-
this.intervals.set(taskInfo.id, timeoutId as any);
|
|
295
|
-
|
|
296
|
-
if (this.config.enableLogging) {
|
|
297
|
-
loggerInstance.info(`Scheduled cron task ${taskInfo.name} to run at ${nextExecution.toISOString()}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private getIntervalMilliseconds(interval: ScheduleInterval): number {
|
|
302
|
-
switch (interval) {
|
|
303
|
-
case ScheduleInterval.MINUTE:
|
|
304
|
-
return 60 * 1000; // 1 minute
|
|
305
|
-
case ScheduleInterval.HOUR:
|
|
306
|
-
return 60 * 60 * 1000; // 1 hour
|
|
307
|
-
case ScheduleInterval.DAILY:
|
|
308
|
-
return 24 * 60 * 60 * 1000; // 24 hours
|
|
309
|
-
case ScheduleInterval.WEEKLY:
|
|
310
|
-
return 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
311
|
-
case ScheduleInterval.MONTHLY:
|
|
312
|
-
return 30 * 24 * 60 * 60 * 1000; // 30 days (approximate)
|
|
313
|
-
default:
|
|
314
|
-
throw new Error(`Unsupported interval: ${interval}`);
|
|
315
|
-
}
|
|
147
|
+
return scheduleJob(this, name, cronExpression, callback);
|
|
316
148
|
}
|
|
317
149
|
|
|
318
150
|
private async executeTask(taskId: string): Promise<void> {
|
|
319
|
-
|
|
320
|
-
// resources (DB pool, cache) are torn down. Without this, a task mid-
|
|
321
|
-
// write during SIGTERM hits a closed DB pool and silently corrupts
|
|
322
|
-
// or loses data.
|
|
323
|
-
const p = this.doExecuteTask(taskId);
|
|
324
|
-
this.inflightTasks.add(p);
|
|
325
|
-
p.finally(() => this.inflightTasks.delete(p));
|
|
326
|
-
return p;
|
|
151
|
+
return executeTask(this, taskId);
|
|
327
152
|
}
|
|
328
153
|
|
|
329
154
|
private async doExecuteTask(taskId: string): Promise<void> {
|
|
330
|
-
|
|
331
|
-
if (!taskInfo || !taskInfo.enabled) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Skip if the previous tick is still executing. Without this guard
|
|
336
|
-
// a slow task with interval < execution-time burns a lock-acquire
|
|
337
|
-
// round-trip every tick and floods the skipped-executions metric
|
|
338
|
-
// (H-SCHED-1). Cheap in-process check before reaching out to PG.
|
|
339
|
-
if (taskInfo.isRunning) {
|
|
340
|
-
this.metrics.skippedExecutions++;
|
|
341
|
-
if (this.config.enableLogging) {
|
|
342
|
-
loggerInstance.debug(`Task ${taskInfo.name} skipped - previous execution still running`);
|
|
343
|
-
}
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (this.metrics.runningTasks >= this.config.maxConcurrentTasks) {
|
|
348
|
-
if (this.config.enableLogging) {
|
|
349
|
-
loggerInstance.warn(`Maximum concurrent tasks reached. Skipping execution of ${taskInfo.name}`);
|
|
350
|
-
}
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Try to acquire distributed lock before executing
|
|
355
|
-
this.metrics.lockAttempts++;
|
|
356
|
-
const lockResult = await this.distributedLock.tryAcquire(taskId);
|
|
357
|
-
|
|
358
|
-
if (!lockResult.acquired) {
|
|
359
|
-
// Another instance is executing this task
|
|
360
|
-
this.metrics.skippedExecutions++;
|
|
361
|
-
|
|
362
|
-
if (this.config.enableLogging) {
|
|
363
|
-
loggerInstance.debug(`Task ${taskInfo.name} skipped - another instance is executing (lock key: ${lockResult.lockKey})`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
this.emitEvent({
|
|
367
|
-
type: 'task.skipped',
|
|
368
|
-
taskId: taskInfo.id,
|
|
369
|
-
timestamp: new Date(),
|
|
370
|
-
data: { reason: 'lock_unavailable', lockKey: lockResult.lockKey.toString() }
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Lock acquired successfully
|
|
377
|
-
this.metrics.locksAcquired++;
|
|
378
|
-
|
|
379
|
-
this.emitEvent({
|
|
380
|
-
type: 'task.lock.acquired',
|
|
381
|
-
taskId: taskInfo.id,
|
|
382
|
-
timestamp: new Date(),
|
|
383
|
-
data: { lockKey: lockResult.lockKey.toString() }
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
taskInfo.isRunning = true;
|
|
387
|
-
taskInfo.lastExecution = new Date();
|
|
388
|
-
this.metrics.runningTasks++;
|
|
389
|
-
|
|
390
|
-
const startTime = Date.now();
|
|
391
|
-
const timeout = taskInfo.options?.timeout || this.config.defaultTimeout;
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
// Create query based on targeting configuration
|
|
395
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
396
|
-
let query: Query<any> | null = null;
|
|
397
|
-
|
|
398
|
-
if (taskInfo.options?.query) {
|
|
399
|
-
// Use custom query function (preferred approach)
|
|
400
|
-
query = taskInfo.options.query();
|
|
401
|
-
} else if (taskInfo.options?.componentTarget) {
|
|
402
|
-
// Use component targeting configuration (deprecated - use query instead)
|
|
403
|
-
const componentTarget = taskInfo.options.componentTarget;
|
|
404
|
-
query = this.buildQueryFromComponentTarget(componentTarget);
|
|
405
|
-
} else if (taskInfo.componentTarget) {
|
|
406
|
-
// Use legacy single component targeting (deprecated - use query instead)
|
|
407
|
-
query = new Query().with(taskInfo.componentTarget);
|
|
408
|
-
}
|
|
409
|
-
// else: time-based task — no entity selection. Handler invoked
|
|
410
|
-
// with no arguments on each tick.
|
|
411
|
-
|
|
412
|
-
// Apply entity limit if specified (can be used with query function)
|
|
413
|
-
if (query && taskInfo.options?.maxEntitiesPerExecution) {
|
|
414
|
-
query.take(taskInfo.options.maxEntitiesPerExecution);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const entities = query ? await query.exec() : [];
|
|
418
|
-
|
|
419
|
-
// Execute the scheduled method with the entities array
|
|
420
|
-
const method = taskInfo.service[taskInfo.methodName];
|
|
421
|
-
if (typeof method !== 'function') {
|
|
422
|
-
throw new Error(`Method ${taskInfo.methodName} not found on service`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Execute with timeout. Time-based tasks receive no entity arg.
|
|
426
|
-
const result = await this.executeWithTimeout(
|
|
427
|
-
query
|
|
428
|
-
? method.call(taskInfo.service, entities)
|
|
429
|
-
: method.call(taskInfo.service),
|
|
430
|
-
timeout,
|
|
431
|
-
taskInfo
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
const duration = Date.now() - startTime;
|
|
435
|
-
taskInfo.executionCount++;
|
|
436
|
-
this.metrics.completedExecutions++;
|
|
437
|
-
this.metrics.totalExecutionTime += duration;
|
|
438
|
-
this.metrics.averageExecutionTime = this.metrics.totalExecutionTime / this.metrics.completedExecutions;
|
|
439
|
-
|
|
440
|
-
// Update task-specific metrics
|
|
441
|
-
this.updateTaskMetrics(taskInfo.id, {
|
|
442
|
-
totalExecutions: taskInfo.executionCount,
|
|
443
|
-
successfulExecutions: (this.metrics.taskMetrics[taskInfo.id]?.successfulExecutions || 0) + 1,
|
|
444
|
-
averageExecutionTime: duration,
|
|
445
|
-
lastExecutionTime: new Date(),
|
|
446
|
-
totalEntitiesProcessed: entities.length
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
if (this.config.enableLogging) {
|
|
450
|
-
loggerInstance.info(`Task ${taskInfo.name} completed successfully in ${duration}ms (processed ${entities.length} entities)`);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
this.emitEvent({
|
|
454
|
-
type: 'task.executed',
|
|
455
|
-
taskId: taskInfo.id,
|
|
456
|
-
timestamp: new Date(),
|
|
457
|
-
data: { duration, entitiesProcessed: entities.length, success: true }
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
} catch (error) {
|
|
461
|
-
const duration = Date.now() - startTime;
|
|
462
|
-
this.metrics.failedExecutions++;
|
|
463
|
-
|
|
464
|
-
// Handle retry logic
|
|
465
|
-
await this.handleTaskFailure(taskInfo, error instanceof Error ? error : new Error(String(error)), duration);
|
|
466
|
-
|
|
467
|
-
if (this.config.enableLogging) {
|
|
468
|
-
loggerInstance.error(`Task ${taskInfo.name} failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
this.emitEvent({
|
|
472
|
-
type: 'task.failed',
|
|
473
|
-
taskId: taskInfo.id,
|
|
474
|
-
timestamp: new Date(),
|
|
475
|
-
data: { duration, error: error instanceof Error ? error.message : String(error) }
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
} finally {
|
|
479
|
-
taskInfo.isRunning = false;
|
|
480
|
-
this.metrics.runningTasks--;
|
|
481
|
-
|
|
482
|
-
// Release the distributed lock
|
|
483
|
-
await this.distributedLock.release(taskId);
|
|
484
|
-
|
|
485
|
-
this.emitEvent({
|
|
486
|
-
type: 'task.lock.released',
|
|
487
|
-
taskId: taskInfo.id,
|
|
488
|
-
timestamp: new Date(),
|
|
489
|
-
data: { lockKey: lockResult.lockKey.toString() }
|
|
490
|
-
});
|
|
491
|
-
}
|
|
155
|
+
return doExecuteTask(this, taskId);
|
|
492
156
|
}
|
|
493
157
|
|
|
494
158
|
public start(): void {
|
|
@@ -510,7 +174,7 @@ export class SchedulerManager {
|
|
|
510
174
|
|
|
511
175
|
// Schedule all registered tasks in priority order
|
|
512
176
|
for (const taskInfo of sortedTasks) {
|
|
513
|
-
|
|
177
|
+
scheduleTask(this, taskInfo);
|
|
514
178
|
}
|
|
515
179
|
|
|
516
180
|
const lockStatus = this.config.distributedLocking !== false ? 'enabled' : 'disabled';
|
|
@@ -577,7 +241,7 @@ export class SchedulerManager {
|
|
|
577
241
|
}
|
|
578
242
|
|
|
579
243
|
public getMetrics(): SchedulerMetrics {
|
|
580
|
-
return
|
|
244
|
+
return getMetrics(this);
|
|
581
245
|
}
|
|
582
246
|
|
|
583
247
|
public getTasks(): ScheduledTaskInfo[] {
|
|
@@ -592,7 +256,7 @@ export class SchedulerManager {
|
|
|
592
256
|
|
|
593
257
|
task.enabled = true;
|
|
594
258
|
if (this.isRunning) {
|
|
595
|
-
|
|
259
|
+
scheduleTask(this, task);
|
|
596
260
|
}
|
|
597
261
|
return true;
|
|
598
262
|
}
|
|
@@ -624,7 +288,7 @@ export class SchedulerManager {
|
|
|
624
288
|
}
|
|
625
289
|
}
|
|
626
290
|
|
|
627
|
-
|
|
291
|
+
public emitEvent(event: SchedulerEvent): void {
|
|
628
292
|
for (const listener of this.eventListeners) {
|
|
629
293
|
try {
|
|
630
294
|
listener(event);
|
|
@@ -638,12 +302,7 @@ export class SchedulerManager {
|
|
|
638
302
|
this.config = { ...this.config, ...config };
|
|
639
303
|
|
|
640
304
|
// Sync distributed lock configuration
|
|
641
|
-
this
|
|
642
|
-
enabled: this.config.distributedLocking ?? true,
|
|
643
|
-
enableLogging: this.config.enableLogging,
|
|
644
|
-
lockTimeout: this.config.lockTimeout ?? 0,
|
|
645
|
-
retryInterval: this.config.lockRetryInterval ?? 100,
|
|
646
|
-
});
|
|
305
|
+
syncLockConfig(this);
|
|
647
306
|
|
|
648
307
|
if (this.config.enableLogging) {
|
|
649
308
|
loggerInstance.info(`Scheduler configuration updated: ${JSON.stringify(config)}`);
|
|
@@ -662,187 +321,28 @@ export class SchedulerManager {
|
|
|
662
321
|
heldLocks: number;
|
|
663
322
|
config: DistributedLockConfig;
|
|
664
323
|
} {
|
|
665
|
-
return
|
|
666
|
-
enabled: this.config.distributedLocking !== false,
|
|
667
|
-
heldLocks: this.distributedLock.getHeldLockCount(),
|
|
668
|
-
config: this.distributedLock.getConfig(),
|
|
669
|
-
};
|
|
324
|
+
return getDistributedLockInfo(this);
|
|
670
325
|
}
|
|
671
326
|
|
|
672
327
|
/**
|
|
673
328
|
* Check if distributed locking is enabled
|
|
674
329
|
*/
|
|
675
330
|
public isDistributedLockingEnabled(): boolean {
|
|
676
|
-
return this
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Execute a task with timeout enforcement.
|
|
681
|
-
*
|
|
682
|
-
* Note: JS has no way to cancel an arbitrary Promise — on timeout the
|
|
683
|
-
* wrapper rejects but the underlying task continues. The second .catch
|
|
684
|
-
* below captures a late rejection after the wrapper already rejected,
|
|
685
|
-
* preventing an unhandled-rejection process crash (H-SCHED-5). The
|
|
686
|
-
* `settled` flag guards against double-settle.
|
|
687
|
-
*/
|
|
688
|
-
private async executeWithTimeout<T>(task: Promise<T>, timeoutMs: number, taskInfo: ScheduledTaskInfo): Promise<T> {
|
|
689
|
-
return new Promise((resolve, reject) => {
|
|
690
|
-
let settled = false;
|
|
691
|
-
const timeoutId = setTimeout(() => {
|
|
692
|
-
if (settled) return;
|
|
693
|
-
settled = true;
|
|
694
|
-
this.metrics.timedOutTasks++;
|
|
695
|
-
this.updateTaskMetrics(taskInfo.id, {
|
|
696
|
-
timeoutCount: (this.metrics.taskMetrics[taskInfo.id]?.timeoutCount || 0) + 1
|
|
697
|
-
});
|
|
698
|
-
const error = new Error(`Task ${taskInfo.name} timed out after ${timeoutMs}ms`);
|
|
699
|
-
this.emitEvent({
|
|
700
|
-
type: 'task.timeout',
|
|
701
|
-
taskId: taskInfo.id,
|
|
702
|
-
timestamp: new Date(),
|
|
703
|
-
data: { timeoutMs, taskName: taskInfo.name }
|
|
704
|
-
});
|
|
705
|
-
reject(error);
|
|
706
|
-
}, timeoutMs);
|
|
707
|
-
|
|
708
|
-
task
|
|
709
|
-
.then((result) => {
|
|
710
|
-
if (settled) return;
|
|
711
|
-
settled = true;
|
|
712
|
-
clearTimeout(timeoutId);
|
|
713
|
-
resolve(result);
|
|
714
|
-
})
|
|
715
|
-
.catch((error) => {
|
|
716
|
-
if (settled) {
|
|
717
|
-
// Late rejection after timeout. Log only — the wrapper
|
|
718
|
-
// promise is already settled, so re-rejecting would be
|
|
719
|
-
// a no-op but the rejection would escape as unhandled.
|
|
720
|
-
loggerInstance.warn({ taskId: taskInfo.id, err: error }, 'Late rejection from scheduled task after timeout');
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
settled = true;
|
|
724
|
-
clearTimeout(timeoutId);
|
|
725
|
-
reject(error);
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Handle task failure with retry logic
|
|
732
|
-
*/
|
|
733
|
-
private async handleTaskFailure(taskInfo: ScheduledTaskInfo, error: Error, duration: number): Promise<void> {
|
|
734
|
-
taskInfo.lastError = error.message;
|
|
735
|
-
|
|
736
|
-
const maxRetries = taskInfo.options?.maxRetries || taskInfo.maxRetries || 0;
|
|
737
|
-
const retryDelay = taskInfo.options?.retryDelay || 1000; // Default 1 second
|
|
738
|
-
|
|
739
|
-
if (taskInfo.retryCount === undefined) {
|
|
740
|
-
taskInfo.retryCount = 0;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if (taskInfo.retryCount < maxRetries) {
|
|
744
|
-
taskInfo.retryCount++;
|
|
745
|
-
this.metrics.retriedTasks++;
|
|
746
|
-
|
|
747
|
-
this.updateTaskMetrics(taskInfo.id, {
|
|
748
|
-
retryCount: taskInfo.retryCount
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
if (this.config.enableLogging) {
|
|
752
|
-
loggerInstance.warn(`Task ${taskInfo.name} failed (attempt ${taskInfo.retryCount}/${maxRetries}), retrying in ${retryDelay}ms: ${error.message}`);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Schedule retry. Track the timer handle in `intervals` under a
|
|
756
|
-
// unique key so `stop()` can clear it (H-SCHED-3); without this
|
|
757
|
-
// the retry fires post-shutdown against a closed DB pool. Also
|
|
758
|
-
// skip the retry if the scheduler is no longer running by the
|
|
759
|
-
// time the timer fires, and rely on the new isRunning guard in
|
|
760
|
-
// doExecuteTask (H-SCHED-1) to prevent retry/tick overlap.
|
|
761
|
-
const retryKey = `${taskInfo.id}:retry:${taskInfo.retryCount}`;
|
|
762
|
-
const retryHandle = setTimeout(async () => {
|
|
763
|
-
this.intervals.delete(retryKey);
|
|
764
|
-
if (!this.isRunning) return;
|
|
765
|
-
await this.executeTask(taskInfo.id);
|
|
766
|
-
}, retryDelay);
|
|
767
|
-
this.intervals.set(retryKey, retryHandle as any);
|
|
768
|
-
|
|
769
|
-
this.emitEvent({
|
|
770
|
-
type: 'task.retry',
|
|
771
|
-
taskId: taskInfo.id,
|
|
772
|
-
timestamp: new Date(),
|
|
773
|
-
data: {
|
|
774
|
-
attempt: taskInfo.retryCount,
|
|
775
|
-
maxRetries,
|
|
776
|
-
retryDelay,
|
|
777
|
-
error: error.message
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
} else {
|
|
781
|
-
// Max retries reached or no retries configured
|
|
782
|
-
this.updateTaskMetrics(taskInfo.id, {
|
|
783
|
-
failedExecutions: (this.metrics.taskMetrics[taskInfo.id]?.failedExecutions || 0) + 1
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
if (this.config.enableLogging) {
|
|
787
|
-
loggerInstance.error(`Task ${taskInfo.name} failed permanently after ${taskInfo.retryCount} attempts: ${error.message}`);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
this.emitEvent({
|
|
791
|
-
type: 'task.failed',
|
|
792
|
-
taskId: taskInfo.id,
|
|
793
|
-
timestamp: new Date(),
|
|
794
|
-
data: {
|
|
795
|
-
duration,
|
|
796
|
-
error: error.message,
|
|
797
|
-
attempts: taskInfo.retryCount,
|
|
798
|
-
maxRetries
|
|
799
|
-
}
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Update task-specific metrics
|
|
806
|
-
*/
|
|
807
|
-
private updateTaskMetrics(taskId: string, updates: Partial<TaskMetrics>): void {
|
|
808
|
-
if (!this.metrics.taskMetrics[taskId]) {
|
|
809
|
-
const taskInfo = this.tasks.get(taskId);
|
|
810
|
-
this.metrics.taskMetrics[taskId] = {
|
|
811
|
-
taskId,
|
|
812
|
-
taskName: taskInfo?.name || 'Unknown',
|
|
813
|
-
totalExecutions: 0,
|
|
814
|
-
successfulExecutions: 0,
|
|
815
|
-
failedExecutions: 0,
|
|
816
|
-
averageExecutionTime: 0,
|
|
817
|
-
totalEntitiesProcessed: 0,
|
|
818
|
-
retryCount: 0,
|
|
819
|
-
timeoutCount: 0
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const metrics = this.metrics.taskMetrics[taskId];
|
|
824
|
-
Object.assign(metrics, updates);
|
|
825
|
-
|
|
826
|
-
// Update rolling averages
|
|
827
|
-
if (updates.averageExecutionTime !== undefined) {
|
|
828
|
-
const currentAvg = metrics.averageExecutionTime;
|
|
829
|
-
const newCount = metrics.totalExecutions;
|
|
830
|
-
metrics.averageExecutionTime = ((currentAvg * (newCount - 1)) + updates.averageExecutionTime) / newCount;
|
|
831
|
-
}
|
|
331
|
+
return isDistributedLockingEnabled(this);
|
|
832
332
|
}
|
|
833
333
|
|
|
834
334
|
/**
|
|
835
335
|
* Get detailed metrics for a specific task
|
|
836
336
|
*/
|
|
837
337
|
public getTaskMetrics(taskId: string): TaskMetrics | null {
|
|
838
|
-
return this
|
|
338
|
+
return getTaskMetrics(this, taskId);
|
|
839
339
|
}
|
|
840
340
|
|
|
841
341
|
/**
|
|
842
342
|
* Get all task metrics
|
|
843
343
|
*/
|
|
844
344
|
public getAllTaskMetrics(): Record<string, TaskMetrics> {
|
|
845
|
-
return
|
|
345
|
+
return getAllTaskMetrics(this);
|
|
846
346
|
}
|
|
847
347
|
|
|
848
348
|
/**
|
|
@@ -858,76 +358,4 @@ export class SchedulerManager {
|
|
|
858
358
|
await this.executeTask(taskId);
|
|
859
359
|
return true;
|
|
860
360
|
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Build a Query object from ComponentTargetConfig
|
|
864
|
-
* @param componentTarget The component targeting configuration
|
|
865
|
-
* @returns A Query object configured with the component targeting
|
|
866
|
-
*/
|
|
867
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
868
|
-
private buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query<any> {
|
|
869
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
870
|
-
let query: Query<any> = new Query();
|
|
871
|
-
|
|
872
|
-
// Handle archetype matching first (most specific)
|
|
873
|
-
if (componentTarget.archetype) {
|
|
874
|
-
// For archetype matching, we need to include all components from the archetype
|
|
875
|
-
const archetypeComponents = this.getArchetypeComponents(componentTarget.archetype);
|
|
876
|
-
for (const component of archetypeComponents) {
|
|
877
|
-
query = query.with(component);
|
|
878
|
-
}
|
|
879
|
-
} else if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
|
|
880
|
-
// Handle multiple archetypes - for simplicity, we'll use the first valid one
|
|
881
|
-
// In a more advanced implementation, you might want to handle OR logic
|
|
882
|
-
const firstArchetype = componentTarget.archetypes.find(archetype => archetype !== undefined);
|
|
883
|
-
if (firstArchetype) {
|
|
884
|
-
const archetypeComponents = this.getArchetypeComponents(firstArchetype);
|
|
885
|
-
for (const component of archetypeComponents) {
|
|
886
|
-
query = query.with(component);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Handle included components
|
|
892
|
-
if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
|
|
893
|
-
const requireAll = componentTarget.requireAllIncluded ?? true;
|
|
894
|
-
if (requireAll) {
|
|
895
|
-
// ALL included components must be present (AND logic)
|
|
896
|
-
for (const component of componentTarget.includeComponents) {
|
|
897
|
-
query = query.with(component);
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
// ANY included component must be present (OR logic)
|
|
901
|
-
// For OR logic with Query API, we need to use a different approach
|
|
902
|
-
// This is a simplified implementation - in practice, you might need custom query logic
|
|
903
|
-
for (const component of componentTarget.includeComponents) {
|
|
904
|
-
query = query.with(component);
|
|
905
|
-
break; // Just use the first one for simplicity
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Handle excluded components
|
|
911
|
-
if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
|
|
912
|
-
for(const component of componentTarget.excludeComponents){
|
|
913
|
-
query = query.without(component);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
return query;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* Extract component classes from an ArcheType
|
|
922
|
-
* @param archetype The archetype to extract components from
|
|
923
|
-
* @returns Array of component classes
|
|
924
|
-
*/
|
|
925
|
-
private getArchetypeComponents(archetype: ArcheType): (new () => BaseComponent)[] {
|
|
926
|
-
// Access the private componentMap from ArcheType
|
|
927
|
-
const componentMap = (archetype as any).componentMap as Record<string, new () => BaseComponent>;
|
|
928
|
-
if (!componentMap) {
|
|
929
|
-
return [];
|
|
930
|
-
}
|
|
931
|
-
return Object.values(componentMap);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
361
|
+
}
|