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.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
@@ -12,12 +12,13 @@ import type {
12
12
  SchedulerConfig,
13
13
  TaskMetrics
14
14
  } from "../types/scheduler.types";
15
- import Query from "./Query";
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 "./Components";
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
- private config: SchedulerConfig;
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: true,
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 || (!taskInfo.componentTarget && !taskInfo.options?.componentTarget) || !taskInfo.interval) {
82
- const error = new Error(`Invalid task info: missing required fields (id, name, componentTarget/componentTarget config, interval)`);
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 component targeting configuration
244
- let query: Query;
245
-
246
- if (taskInfo.options?.componentTarget) {
247
- // Use new component targeting configuration
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 component filters if specified
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
- private buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query {
646
- let query = new Query();
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) {