bunsane 0.1.4 → 0.2.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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
package/core/SchedulerManager.ts
CHANGED
|
@@ -12,12 +12,13 @@ import type {
|
|
|
12
12
|
SchedulerConfig,
|
|
13
13
|
TaskMetrics
|
|
14
14
|
} from "../types/scheduler.types";
|
|
15
|
-
import Query from "
|
|
15
|
+
import { Query } from "../query/Query";
|
|
16
16
|
import { Entity } from "./Entity";
|
|
17
17
|
import { CronParser } from "../utils/cronParser";
|
|
18
18
|
import type { ComponentTargetConfig } from "./EntityHookManager";
|
|
19
19
|
import ArcheType from "./ArcheType";
|
|
20
|
-
import { BaseComponent } from "./
|
|
20
|
+
import { BaseComponent } from "./components";
|
|
21
|
+
import { DistributedLock, type DistributedLockConfig } from "./scheduler/DistributedLock";
|
|
21
22
|
|
|
22
23
|
const loggerInstance = logger.child({ scope: "SchedulerManager" });
|
|
23
24
|
|
|
@@ -27,7 +28,8 @@ export class SchedulerManager {
|
|
|
27
28
|
private intervals: Map<string, NodeJS.Timeout> = new Map();
|
|
28
29
|
private isRunning: boolean = false;
|
|
29
30
|
private eventListeners: SchedulerEventCallback[] = [];
|
|
30
|
-
|
|
31
|
+
public config: SchedulerConfig;
|
|
32
|
+
private distributedLock: DistributedLock;
|
|
31
33
|
private metrics: SchedulerMetrics = {
|
|
32
34
|
totalTasks: 0,
|
|
33
35
|
runningTasks: 0,
|
|
@@ -37,7 +39,10 @@ export class SchedulerManager {
|
|
|
37
39
|
totalExecutionTime: 0,
|
|
38
40
|
timedOutTasks: 0,
|
|
39
41
|
retriedTasks: 0,
|
|
40
|
-
taskMetrics: {}
|
|
42
|
+
taskMetrics: {},
|
|
43
|
+
skippedExecutions: 0,
|
|
44
|
+
lockAttempts: 0,
|
|
45
|
+
locksAcquired: 0
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
private constructor() {
|
|
@@ -45,10 +50,21 @@ export class SchedulerManager {
|
|
|
45
50
|
enabled: true,
|
|
46
51
|
maxConcurrentTasks: 5,
|
|
47
52
|
defaultTimeout: 30000, // 30 seconds
|
|
48
|
-
enableLogging:
|
|
49
|
-
runOnStart: true
|
|
53
|
+
enableLogging: false,
|
|
54
|
+
runOnStart: true,
|
|
55
|
+
distributedLocking: true, // Enable by default for multi-instance safety
|
|
56
|
+
lockTimeout: 0, // No retry by default - skip if can't acquire
|
|
57
|
+
lockRetryInterval: 100,
|
|
50
58
|
};
|
|
51
59
|
|
|
60
|
+
// Initialize distributed lock with config
|
|
61
|
+
this.distributedLock = new DistributedLock({
|
|
62
|
+
enabled: this.config.distributedLocking ?? true,
|
|
63
|
+
enableLogging: this.config.enableLogging,
|
|
64
|
+
lockTimeout: this.config.lockTimeout ?? 0,
|
|
65
|
+
retryInterval: this.config.lockRetryInterval ?? 100,
|
|
66
|
+
});
|
|
67
|
+
|
|
52
68
|
this.initializeLifecycleIntegration();
|
|
53
69
|
}
|
|
54
70
|
|
|
@@ -63,7 +79,6 @@ export class SchedulerManager {
|
|
|
63
79
|
ApplicationLifecycle.addPhaseListener((event) => {
|
|
64
80
|
const phase = event.detail;
|
|
65
81
|
if (phase === ApplicationPhase.APPLICATION_READY) {
|
|
66
|
-
logger.info("Scheduler initialized and ready");
|
|
67
82
|
if (this.config.runOnStart) {
|
|
68
83
|
this.start();
|
|
69
84
|
}
|
|
@@ -78,8 +93,15 @@ export class SchedulerManager {
|
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
// Validate task info
|
|
81
|
-
if (!taskInfo.id || !taskInfo.name ||
|
|
82
|
-
const error = new Error(`Invalid task info: missing required fields (id, name,
|
|
96
|
+
if (!taskInfo.id || !taskInfo.name || !taskInfo.interval) {
|
|
97
|
+
const error = new Error(`Invalid task info: missing required fields (id, name, interval)`);
|
|
98
|
+
loggerInstance.error(`Failed to register task: ${error.message}`);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate query configuration
|
|
103
|
+
if (!taskInfo.options?.query && !taskInfo.options?.componentTarget && !taskInfo.componentTarget) {
|
|
104
|
+
const error = new Error(`Invalid task info: must provide either query function, componentTarget config, or legacy componentTarget`);
|
|
83
105
|
loggerInstance.error(`Failed to register task: ${error.message}`);
|
|
84
106
|
throw error;
|
|
85
107
|
}
|
|
@@ -118,6 +140,59 @@ export class SchedulerManager {
|
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Schedule a simple job with a cron expression and callback.
|
|
145
|
+
* This is a simpler alternative to registerTask for jobs that don't need
|
|
146
|
+
* entity-component system integration.
|
|
147
|
+
*/
|
|
148
|
+
public scheduleJob(name: string, cronExpression: string, callback: () => Promise<void> | void): { cancel: () => void } {
|
|
149
|
+
const jobId = `job_${name}_${Date.now()}`;
|
|
150
|
+
|
|
151
|
+
// Validate cron expression
|
|
152
|
+
const validation = CronParser.validate(cronExpression);
|
|
153
|
+
if (!validation.isValid) {
|
|
154
|
+
throw new Error(`Invalid cron expression for job "${name}": ${validation.error}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
|
|
160
|
+
const scheduleNextExecution = () => {
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
|
|
163
|
+
const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
|
|
164
|
+
if (!nextExecution) {
|
|
165
|
+
loggerInstance.warn(`Unable to calculate next execution for job "${name}"`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const delay = nextExecution.getTime() - Date.now();
|
|
170
|
+
timeoutId = setTimeout(async () => {
|
|
171
|
+
if (cancelled) return;
|
|
172
|
+
try {
|
|
173
|
+
await callback();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
loggerInstance.error(`Job "${name}" failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
+
}
|
|
177
|
+
scheduleNextExecution();
|
|
178
|
+
}, delay);
|
|
179
|
+
|
|
180
|
+
this.intervals.set(jobId, timeoutId as any);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
scheduleNextExecution();
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
cancel: () => {
|
|
187
|
+
cancelled = true;
|
|
188
|
+
if (timeoutId) {
|
|
189
|
+
clearTimeout(timeoutId);
|
|
190
|
+
this.intervals.delete(jobId);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
121
196
|
private scheduleTask(taskInfo: ScheduledTaskInfo): void {
|
|
122
197
|
try {
|
|
123
198
|
if (taskInfo.interval === ScheduleInterval.CRON) {
|
|
@@ -134,6 +209,13 @@ export class SchedulerManager {
|
|
|
134
209
|
private scheduleIntervalTask(taskInfo: ScheduledTaskInfo): void {
|
|
135
210
|
const intervalMs = this.getIntervalMilliseconds(taskInfo.interval);
|
|
136
211
|
|
|
212
|
+
// Clear any existing interval for this task before creating a new one
|
|
213
|
+
const existingInterval = this.intervals.get(taskInfo.id);
|
|
214
|
+
if (existingInterval) {
|
|
215
|
+
clearInterval(existingInterval);
|
|
216
|
+
this.intervals.delete(taskInfo.id);
|
|
217
|
+
}
|
|
218
|
+
|
|
137
219
|
// For very long intervals (monthly), use a different approach
|
|
138
220
|
if (intervalMs > 24 * 60 * 60 * 1000) { // More than 24 hours
|
|
139
221
|
this.scheduleLongIntervalTask(taskInfo, intervalMs);
|
|
@@ -188,6 +270,12 @@ export class SchedulerManager {
|
|
|
188
270
|
|
|
189
271
|
taskInfo.nextExecution = nextExecution;
|
|
190
272
|
|
|
273
|
+
// Clear any existing timeout for this task before creating a new one
|
|
274
|
+
const existingTimeout = this.intervals.get(taskInfo.id);
|
|
275
|
+
if (existingTimeout) {
|
|
276
|
+
clearTimeout(existingTimeout as any);
|
|
277
|
+
}
|
|
278
|
+
|
|
191
279
|
// Schedule the task to run at the calculated time
|
|
192
280
|
const timeoutId = setTimeout(async () => {
|
|
193
281
|
await this.executeTask(taskInfo.id);
|
|
@@ -232,6 +320,38 @@ export class SchedulerManager {
|
|
|
232
320
|
return;
|
|
233
321
|
}
|
|
234
322
|
|
|
323
|
+
// Try to acquire distributed lock before executing
|
|
324
|
+
this.metrics.lockAttempts++;
|
|
325
|
+
const lockResult = await this.distributedLock.tryAcquire(taskId);
|
|
326
|
+
|
|
327
|
+
if (!lockResult.acquired) {
|
|
328
|
+
// Another instance is executing this task
|
|
329
|
+
this.metrics.skippedExecutions++;
|
|
330
|
+
|
|
331
|
+
if (this.config.enableLogging) {
|
|
332
|
+
loggerInstance.debug(`Task ${taskInfo.name} skipped - another instance is executing (lock key: ${lockResult.lockKey})`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.emitEvent({
|
|
336
|
+
type: 'task.skipped',
|
|
337
|
+
taskId: taskInfo.id,
|
|
338
|
+
timestamp: new Date(),
|
|
339
|
+
data: { reason: 'lock_unavailable', lockKey: lockResult.lockKey.toString() }
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Lock acquired successfully
|
|
346
|
+
this.metrics.locksAcquired++;
|
|
347
|
+
|
|
348
|
+
this.emitEvent({
|
|
349
|
+
type: 'task.lock.acquired',
|
|
350
|
+
taskId: taskInfo.id,
|
|
351
|
+
timestamp: new Date(),
|
|
352
|
+
data: { lockKey: lockResult.lockKey.toString() }
|
|
353
|
+
});
|
|
354
|
+
|
|
235
355
|
taskInfo.isRunning = true;
|
|
236
356
|
taskInfo.lastExecution = new Date();
|
|
237
357
|
this.metrics.runningTasks++;
|
|
@@ -240,51 +360,25 @@ export class SchedulerManager {
|
|
|
240
360
|
const timeout = taskInfo.options?.timeout || this.config.defaultTimeout;
|
|
241
361
|
|
|
242
362
|
try {
|
|
243
|
-
// Create query based on
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
363
|
+
// Create query based on targeting configuration
|
|
364
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
365
|
+
let query: Query<any>;
|
|
366
|
+
|
|
367
|
+
if (taskInfo.options?.query) {
|
|
368
|
+
// Use custom query function (preferred approach)
|
|
369
|
+
query = taskInfo.options.query();
|
|
370
|
+
} else if (taskInfo.options?.componentTarget) {
|
|
371
|
+
// Use component targeting configuration (deprecated - use query instead)
|
|
248
372
|
const componentTarget = taskInfo.options.componentTarget;
|
|
249
373
|
query = this.buildQueryFromComponentTarget(componentTarget);
|
|
250
374
|
} else if (taskInfo.componentTarget) {
|
|
251
|
-
// Use legacy single component targeting
|
|
375
|
+
// Use legacy single component targeting (deprecated - use query instead)
|
|
252
376
|
query = new Query().with(taskInfo.componentTarget);
|
|
253
377
|
} else {
|
|
254
|
-
throw new Error('No component target specified');
|
|
378
|
+
throw new Error('No query function or component target specified');
|
|
255
379
|
}
|
|
256
380
|
|
|
257
|
-
// Apply
|
|
258
|
-
if (taskInfo.options?.componentFilters && taskInfo.options.componentFilters.length > 0) {
|
|
259
|
-
// Group filters by component for the Query API
|
|
260
|
-
const filtersByComponent = new Map<string, any[]>();
|
|
261
|
-
|
|
262
|
-
for (const filter of taskInfo.options.componentFilters) {
|
|
263
|
-
// For now, we'll assume filters are for the main component
|
|
264
|
-
// In a more advanced implementation, we could support filters for different components
|
|
265
|
-
const mainComponent = taskInfo.componentTarget || taskInfo.options?.componentTarget?.includeComponents?.[0];
|
|
266
|
-
if (mainComponent) {
|
|
267
|
-
const componentName = typeof mainComponent === 'function' ? mainComponent.name : 'unknown';
|
|
268
|
-
if (!filtersByComponent.has(componentName)) {
|
|
269
|
-
filtersByComponent.set(componentName, []);
|
|
270
|
-
}
|
|
271
|
-
filtersByComponent.get(componentName)!.push(filter);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Apply filters to components
|
|
276
|
-
for (const [componentName, filters] of filtersByComponent.entries()) {
|
|
277
|
-
// This is a simplified implementation - in practice, you'd need to map component names to actual component classes
|
|
278
|
-
if (filters.length > 0) {
|
|
279
|
-
// For legacy compatibility, apply to the main component
|
|
280
|
-
if (taskInfo.componentTarget) {
|
|
281
|
-
query.with(taskInfo.componentTarget, Query.filters(...filters));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Apply entity limit if specified
|
|
381
|
+
// Apply entity limit if specified (can be used with query function)
|
|
288
382
|
if (taskInfo.options?.maxEntitiesPerExecution) {
|
|
289
383
|
query.take(taskInfo.options.maxEntitiesPerExecution);
|
|
290
384
|
}
|
|
@@ -351,6 +445,16 @@ export class SchedulerManager {
|
|
|
351
445
|
} finally {
|
|
352
446
|
taskInfo.isRunning = false;
|
|
353
447
|
this.metrics.runningTasks--;
|
|
448
|
+
|
|
449
|
+
// Release the distributed lock
|
|
450
|
+
await this.distributedLock.release(taskId);
|
|
451
|
+
|
|
452
|
+
this.emitEvent({
|
|
453
|
+
type: 'task.lock.released',
|
|
454
|
+
taskId: taskInfo.id,
|
|
455
|
+
timestamp: new Date(),
|
|
456
|
+
data: { lockKey: lockResult.lockKey.toString() }
|
|
457
|
+
});
|
|
354
458
|
}
|
|
355
459
|
}
|
|
356
460
|
|
|
@@ -376,18 +480,19 @@ export class SchedulerManager {
|
|
|
376
480
|
this.scheduleTask(taskInfo);
|
|
377
481
|
}
|
|
378
482
|
|
|
483
|
+
const lockStatus = this.config.distributedLocking !== false ? 'enabled' : 'disabled';
|
|
379
484
|
if (this.config.enableLogging) {
|
|
380
|
-
loggerInstance.info(`Scheduler started with ${this.tasks.size} tasks (sorted by priority)`);
|
|
485
|
+
loggerInstance.info(`Scheduler started with ${this.tasks.size} tasks (sorted by priority, distributed locking: ${lockStatus})`);
|
|
381
486
|
}
|
|
382
487
|
|
|
383
488
|
this.emitEvent({
|
|
384
489
|
type: 'scheduler.started',
|
|
385
490
|
timestamp: new Date(),
|
|
386
|
-
data: { taskCount: this.tasks.size }
|
|
491
|
+
data: { taskCount: this.tasks.size, distributedLocking: this.config.distributedLocking !== false }
|
|
387
492
|
});
|
|
388
493
|
}
|
|
389
494
|
|
|
390
|
-
public stop(): void {
|
|
495
|
+
public async stop(): Promise<void> {
|
|
391
496
|
if (!this.isRunning) {
|
|
392
497
|
loggerInstance.warn("Scheduler is not running");
|
|
393
498
|
return;
|
|
@@ -402,6 +507,9 @@ export class SchedulerManager {
|
|
|
402
507
|
}
|
|
403
508
|
this.intervals.clear();
|
|
404
509
|
|
|
510
|
+
// Release all distributed locks held by this instance
|
|
511
|
+
await this.distributedLock.releaseAll();
|
|
512
|
+
|
|
405
513
|
if (this.config.enableLogging) {
|
|
406
514
|
loggerInstance.info("Scheduler stopped");
|
|
407
515
|
}
|
|
@@ -472,6 +580,15 @@ export class SchedulerManager {
|
|
|
472
580
|
|
|
473
581
|
public updateConfig(config: Partial<SchedulerConfig>): void {
|
|
474
582
|
this.config = { ...this.config, ...config };
|
|
583
|
+
|
|
584
|
+
// Sync distributed lock configuration
|
|
585
|
+
this.distributedLock.updateConfig({
|
|
586
|
+
enabled: this.config.distributedLocking ?? true,
|
|
587
|
+
enableLogging: this.config.enableLogging,
|
|
588
|
+
lockTimeout: this.config.lockTimeout ?? 0,
|
|
589
|
+
retryInterval: this.config.lockRetryInterval ?? 100,
|
|
590
|
+
});
|
|
591
|
+
|
|
475
592
|
if (this.config.enableLogging) {
|
|
476
593
|
loggerInstance.info(`Scheduler configuration updated: ${JSON.stringify(config)}`);
|
|
477
594
|
}
|
|
@@ -481,6 +598,28 @@ export class SchedulerManager {
|
|
|
481
598
|
return { ...this.config };
|
|
482
599
|
}
|
|
483
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Get distributed lock configuration and status
|
|
603
|
+
*/
|
|
604
|
+
public getDistributedLockInfo(): {
|
|
605
|
+
enabled: boolean;
|
|
606
|
+
heldLocks: number;
|
|
607
|
+
config: DistributedLockConfig;
|
|
608
|
+
} {
|
|
609
|
+
return {
|
|
610
|
+
enabled: this.config.distributedLocking !== false,
|
|
611
|
+
heldLocks: this.distributedLock.getHeldLockCount(),
|
|
612
|
+
config: this.distributedLock.getConfig(),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Check if distributed locking is enabled
|
|
618
|
+
*/
|
|
619
|
+
public isDistributedLockingEnabled(): boolean {
|
|
620
|
+
return this.config.distributedLocking !== false;
|
|
621
|
+
}
|
|
622
|
+
|
|
484
623
|
/**
|
|
485
624
|
* Execute a task with timeout enforcement
|
|
486
625
|
*/
|
|
@@ -642,8 +781,10 @@ export class SchedulerManager {
|
|
|
642
781
|
* @param componentTarget The component targeting configuration
|
|
643
782
|
* @returns A Query object configured with the component targeting
|
|
644
783
|
*/
|
|
645
|
-
|
|
646
|
-
|
|
784
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
785
|
+
private buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query<any> {
|
|
786
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
787
|
+
let query: Query<any> = new Query();
|
|
647
788
|
|
|
648
789
|
// Handle archetype matching first (most specific)
|
|
649
790
|
if (componentTarget.archetype) {
|