bunsane 0.3.2 → 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.
Files changed (214) hide show
  1. package/CHANGELOG.md +445 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +85 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +4 -4
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +16 -8
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +364 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/pendingOps.ts +72 -0
  30. package/core/entity/saveEntity.ts +377 -0
  31. package/core/hooks/dispatcher.ts +439 -0
  32. package/core/hooks/guards.ts +155 -0
  33. package/core/hooks/registry.ts +247 -0
  34. package/core/metadata/definitions/Component.ts +1 -1
  35. package/core/metadata/index.ts +15 -4
  36. package/core/middleware/RateLimit.ts +102 -105
  37. package/core/middleware/RequestId.ts +2 -9
  38. package/core/middleware/SecurityHeaders.ts +2 -11
  39. package/core/middleware/headers.ts +28 -0
  40. package/core/remote/OutboxWorker.ts +213 -183
  41. package/core/remote/RemoteManager.ts +401 -400
  42. package/core/remote/types.ts +153 -151
  43. package/core/requestScope.ts +34 -0
  44. package/core/scheduler/cronEvaluator.ts +174 -0
  45. package/core/scheduler/lifecycleHooks.ts +21 -0
  46. package/core/scheduler/lockCoordinator.ts +27 -0
  47. package/core/scheduler/metrics.ts +14 -0
  48. package/core/scheduler/taskRunner.ts +420 -0
  49. package/database/DatabaseHelper.ts +128 -101
  50. package/database/IndexingStrategy.ts +72 -2
  51. package/database/PreparedStatementCache.ts +8 -2
  52. package/database/cancellable.ts +35 -22
  53. package/database/index.ts +15 -3
  54. package/database/instrumentedDb.ts +141 -141
  55. package/endpoints/archetypes.ts +2 -8
  56. package/endpoints/tables.ts +6 -1
  57. package/gql/index.ts +1 -1
  58. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  59. package/package.json +22 -1
  60. package/query/CTENode.ts +5 -3
  61. package/query/ComponentInclusionNode.ts +240 -13
  62. package/query/OrNode.ts +6 -5
  63. package/query/Query.ts +157 -46
  64. package/query/QueryContext.ts +6 -0
  65. package/query/QueryDAG.ts +7 -2
  66. package/query/membershipSource.ts +66 -0
  67. package/storage/LocalStorageProvider.ts +8 -3
  68. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  69. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  70. package/studio/{index.html → dist/index.html} +3 -2
  71. package/swagger/generator.ts +11 -1
  72. package/upload/UploadManager.ts +8 -6
  73. package/utils/uuid.ts +40 -10
  74. package/.claude/scheduled_tasks.lock +0 -1
  75. package/.claude/settings.local.json +0 -47
  76. package/.prettierrc +0 -4
  77. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  78. package/.serena/memories/architecture.md +0 -154
  79. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  80. package/.serena/memories/code_style_and_conventions.md +0 -76
  81. package/.serena/memories/project_overview.md +0 -43
  82. package/.serena/memories/schema-dsl-plan.md +0 -107
  83. package/.serena/memories/suggested_commands.md +0 -80
  84. package/.serena/memories/typescript-compilation-status.md +0 -54
  85. package/.serena/project.yml +0 -114
  86. package/BunSane.jpg +0 -0
  87. package/CLAUDE.md +0 -198
  88. package/TODO.md +0 -2
  89. package/bun.lock +0 -302
  90. package/bunfig.toml +0 -10
  91. package/docs/RFC_APP_REFACTOR.md +0 -248
  92. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  93. package/docs/SCALABILITY_PLAN.md +0 -175
  94. package/studio/bun.lock +0 -482
  95. package/studio/package.json +0 -39
  96. package/studio/postcss.config.js +0 -6
  97. package/studio/src/components/DataTable.tsx +0 -211
  98. package/studio/src/components/Layout.tsx +0 -13
  99. package/studio/src/components/PageContainer.tsx +0 -9
  100. package/studio/src/components/PageHeader.tsx +0 -13
  101. package/studio/src/components/SearchBar.tsx +0 -57
  102. package/studio/src/components/Sidebar.tsx +0 -294
  103. package/studio/src/components/ui/button.tsx +0 -56
  104. package/studio/src/components/ui/checkbox.tsx +0 -26
  105. package/studio/src/components/ui/input.tsx +0 -25
  106. package/studio/src/hooks/useDataTable.ts +0 -131
  107. package/studio/src/index.css +0 -36
  108. package/studio/src/lib/api.ts +0 -186
  109. package/studio/src/lib/utils.ts +0 -13
  110. package/studio/src/main.tsx +0 -17
  111. package/studio/src/pages/ArcheType.tsx +0 -239
  112. package/studio/src/pages/Components.tsx +0 -124
  113. package/studio/src/pages/EntityInspector.tsx +0 -302
  114. package/studio/src/pages/QueryRunner.tsx +0 -246
  115. package/studio/src/pages/Table.tsx +0 -94
  116. package/studio/src/pages/Welcome.tsx +0 -241
  117. package/studio/src/routes.tsx +0 -45
  118. package/studio/src/store/archeTypeSettings.ts +0 -30
  119. package/studio/src/store/studio.ts +0 -65
  120. package/studio/src/utils/columnHelpers.tsx +0 -114
  121. package/studio/studio-instructions.md +0 -81
  122. package/studio/tailwind.config.js +0 -77
  123. package/studio/utils.ts +0 -54
  124. package/studio/vite.config.js +0 -19
  125. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  126. package/tests/benchmark/bunfig.toml +0 -9
  127. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  128. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  129. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  130. package/tests/benchmark/fixtures/index.ts +0 -6
  131. package/tests/benchmark/index.ts +0 -22
  132. package/tests/benchmark/noop-preload.ts +0 -3
  133. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  134. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  135. package/tests/benchmark/runners/index.ts +0 -4
  136. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  137. package/tests/benchmark/scripts/generate-db.ts +0 -344
  138. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  139. package/tests/e2e/http.test.ts +0 -130
  140. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  141. package/tests/fixtures/components/TestOrder.ts +0 -23
  142. package/tests/fixtures/components/TestProduct.ts +0 -23
  143. package/tests/fixtures/components/TestUser.ts +0 -20
  144. package/tests/fixtures/components/index.ts +0 -6
  145. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  146. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  147. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  148. package/tests/helpers/MockRedisClient.ts +0 -113
  149. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  150. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  151. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  152. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  153. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  154. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  155. package/tests/integration/query/Query.abort.test.ts +0 -66
  156. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  157. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  158. package/tests/integration/query/Query.exec.test.ts +0 -576
  159. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  160. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  161. package/tests/integration/remote/dlq.test.ts +0 -175
  162. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  163. package/tests/integration/remote/outbox.test.ts +0 -130
  164. package/tests/integration/remote/rpc.test.ts +0 -177
  165. package/tests/pglite-setup.ts +0 -62
  166. package/tests/setup.ts +0 -164
  167. package/tests/stress/BenchmarkRunner.ts +0 -203
  168. package/tests/stress/DataSeeder.ts +0 -190
  169. package/tests/stress/StressTestReporter.ts +0 -229
  170. package/tests/stress/cursor-perf-test.ts +0 -171
  171. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  172. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  173. package/tests/stress/index.ts +0 -7
  174. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  175. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  176. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  177. package/tests/unit/BatchLoader.test.ts +0 -196
  178. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  179. package/tests/unit/cache/CacheManager.test.ts +0 -498
  180. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  181. package/tests/unit/cache/RedisCache.test.ts +0 -411
  182. package/tests/unit/database/cancellable.test.ts +0 -81
  183. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  184. package/tests/unit/entity/Entity.components.test.ts +0 -317
  185. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  186. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  187. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  188. package/tests/unit/entity/Entity.test.ts +0 -345
  189. package/tests/unit/gql/depthLimit.test.ts +0 -203
  190. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  191. package/tests/unit/health/Health.test.ts +0 -129
  192. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  193. package/tests/unit/middleware/Middleware.test.ts +0 -98
  194. package/tests/unit/middleware/RequestId.test.ts +0 -54
  195. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  196. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  197. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  198. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  199. package/tests/unit/query/Query.test.ts +0 -310
  200. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  201. package/tests/unit/remote/RemoteError.test.ts +0 -55
  202. package/tests/unit/remote/decorators.test.ts +0 -195
  203. package/tests/unit/remote/metrics.test.ts +0 -115
  204. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  205. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  206. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  207. package/tests/unit/schema/schema-integration.test.ts +0 -426
  208. package/tests/unit/schema/schema.test.ts +0 -580
  209. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  210. package/tests/unit/upload/RestUpload.test.ts +0 -267
  211. package/tests/unit/validateEnv.test.ts +0 -82
  212. package/tests/utils/entity-tracker.ts +0 -57
  213. package/tests/utils/index.ts +0 -13
  214. package/tests/utils/test-context.ts +0 -149
@@ -0,0 +1,420 @@
1
+ import { logger } from "../Logger";
2
+ import { Query } from "../../query/Query";
3
+ import ArcheType from "../ArcheType";
4
+ import { BaseComponent } from "../components";
5
+ import type { ScheduledTaskInfo, SchedulerMetrics, TaskMetrics } from "../../types/scheduler.types";
6
+ import type { ComponentTargetConfig } from "../EntityHookManager";
7
+ import type { SchedulerManager } from "../SchedulerManager";
8
+
9
+ const loggerInstance = logger.child({ scope: "SchedulerManager" });
10
+
11
+ export function updateTaskMetrics(manager: SchedulerManager, taskId: string, updates: Partial<TaskMetrics>): void {
12
+ if (!manager.metrics.taskMetrics[taskId]) {
13
+ const taskInfo = manager.tasks.get(taskId);
14
+ manager.metrics.taskMetrics[taskId] = {
15
+ taskId,
16
+ taskName: taskInfo?.name || 'Unknown',
17
+ totalExecutions: 0,
18
+ successfulExecutions: 0,
19
+ failedExecutions: 0,
20
+ averageExecutionTime: 0,
21
+ totalEntitiesProcessed: 0,
22
+ retryCount: 0,
23
+ timeoutCount: 0
24
+ };
25
+ }
26
+
27
+ const metrics = manager.metrics.taskMetrics[taskId];
28
+ Object.assign(metrics, updates);
29
+
30
+ // Update rolling averages
31
+ if (updates.averageExecutionTime !== undefined) {
32
+ const currentAvg = metrics.averageExecutionTime;
33
+ const newCount = metrics.totalExecutions;
34
+ metrics.averageExecutionTime = ((currentAvg * (newCount - 1)) + updates.averageExecutionTime) / newCount;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Execute a task with timeout enforcement.
40
+ *
41
+ * Note: JS has no way to cancel an arbitrary Promise — on timeout the
42
+ * wrapper rejects but the underlying task continues. The second .catch
43
+ * below captures a late rejection after the wrapper already rejected,
44
+ * preventing an unhandled-rejection process crash (H-SCHED-5). The
45
+ * `settled` flag guards against double-settle.
46
+ */
47
+ export async function executeWithTimeout<T>(
48
+ manager: SchedulerManager,
49
+ task: Promise<T>,
50
+ timeoutMs: number,
51
+ taskInfo: ScheduledTaskInfo
52
+ ): Promise<T> {
53
+ return new Promise((resolve, reject) => {
54
+ let settled = false;
55
+ const timeoutId = setTimeout(() => {
56
+ if (settled) return;
57
+ settled = true;
58
+ manager.metrics.timedOutTasks++;
59
+ updateTaskMetrics(manager, taskInfo.id, {
60
+ timeoutCount: (manager.metrics.taskMetrics[taskInfo.id]?.timeoutCount || 0) + 1
61
+ });
62
+ const error = new Error(`Task ${taskInfo.name} timed out after ${timeoutMs}ms`);
63
+ manager.emitEvent({
64
+ type: 'task.timeout',
65
+ taskId: taskInfo.id,
66
+ timestamp: new Date(),
67
+ data: { timeoutMs, taskName: taskInfo.name }
68
+ });
69
+ reject(error);
70
+ }, timeoutMs);
71
+
72
+ task
73
+ .then((result) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ clearTimeout(timeoutId);
77
+ resolve(result);
78
+ })
79
+ .catch((error) => {
80
+ if (settled) {
81
+ // Late rejection after timeout. Log only — the wrapper
82
+ // promise is already settled, so re-rejecting would be
83
+ // a no-op but the rejection would escape as unhandled.
84
+ loggerInstance.warn({ taskId: taskInfo.id, err: error }, 'Late rejection from scheduled task after timeout');
85
+ return;
86
+ }
87
+ settled = true;
88
+ clearTimeout(timeoutId);
89
+ reject(error);
90
+ });
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Handle task failure with retry logic
96
+ */
97
+ export async function handleTaskFailure(
98
+ manager: SchedulerManager,
99
+ taskInfo: ScheduledTaskInfo,
100
+ error: Error,
101
+ duration: number
102
+ ): Promise<void> {
103
+ taskInfo.lastError = error.message;
104
+
105
+ const maxRetries = taskInfo.options?.maxRetries || taskInfo.maxRetries || 0;
106
+ const retryDelay = taskInfo.options?.retryDelay || 1000; // Default 1 second
107
+
108
+ if (taskInfo.retryCount === undefined) {
109
+ taskInfo.retryCount = 0;
110
+ }
111
+
112
+ if (taskInfo.retryCount < maxRetries) {
113
+ taskInfo.retryCount++;
114
+ manager.metrics.retriedTasks++;
115
+
116
+ updateTaskMetrics(manager, taskInfo.id, {
117
+ retryCount: taskInfo.retryCount
118
+ });
119
+
120
+ if (manager.config.enableLogging) {
121
+ loggerInstance.warn(`Task ${taskInfo.name} failed (attempt ${taskInfo.retryCount}/${maxRetries}), retrying in ${retryDelay}ms: ${error.message}`);
122
+ }
123
+
124
+ // Schedule retry. Track the timer handle in `intervals` under a
125
+ // unique key so `stop()` can clear it (H-SCHED-3); without this
126
+ // the retry fires post-shutdown against a closed DB pool. Also
127
+ // skip the retry if the scheduler is no longer running by the
128
+ // time the timer fires, and rely on the new isRunning guard in
129
+ // doExecuteTask (H-SCHED-1) to prevent retry/tick overlap.
130
+ const retryKey = `${taskInfo.id}:retry:${taskInfo.retryCount}`;
131
+ const retryHandle = setTimeout(async () => {
132
+ manager.intervals.delete(retryKey);
133
+ if (!manager.isRunning) return;
134
+ await (manager as any).executeTask(taskInfo.id);
135
+ }, retryDelay);
136
+ manager.intervals.set(retryKey, retryHandle as any);
137
+
138
+ manager.emitEvent({
139
+ type: 'task.retry',
140
+ taskId: taskInfo.id,
141
+ timestamp: new Date(),
142
+ data: {
143
+ attempt: taskInfo.retryCount,
144
+ maxRetries,
145
+ retryDelay,
146
+ error: error.message
147
+ }
148
+ });
149
+ } else {
150
+ // Max retries reached or no retries configured
151
+ updateTaskMetrics(manager, taskInfo.id, {
152
+ failedExecutions: (manager.metrics.taskMetrics[taskInfo.id]?.failedExecutions || 0) + 1
153
+ });
154
+
155
+ if (manager.config.enableLogging) {
156
+ loggerInstance.error(`Task ${taskInfo.name} failed permanently after ${taskInfo.retryCount} attempts: ${error.message}`);
157
+ }
158
+
159
+ manager.emitEvent({
160
+ type: 'task.failed',
161
+ taskId: taskInfo.id,
162
+ timestamp: new Date(),
163
+ data: {
164
+ duration,
165
+ error: error.message,
166
+ attempts: taskInfo.retryCount,
167
+ maxRetries
168
+ }
169
+ });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Build a Query object from ComponentTargetConfig
175
+ * @param componentTarget The component targeting configuration
176
+ * @returns A Query object configured with the component targeting
177
+ */
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ export function buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query<any> {
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ let query: Query<any> = new Query();
182
+
183
+ // Handle archetype matching first (most specific)
184
+ if (componentTarget.archetype) {
185
+ // For archetype matching, we need to include all components from the archetype
186
+ const archetypeComponents = getArchetypeComponents(componentTarget.archetype);
187
+ for (const component of archetypeComponents) {
188
+ query = query.with(component);
189
+ }
190
+ } else if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
191
+ // Handle multiple archetypes - for simplicity, we'll use the first valid one
192
+ // In a more advanced implementation, you might want to handle OR logic
193
+ const firstArchetype = componentTarget.archetypes.find(archetype => archetype !== undefined);
194
+ if (firstArchetype) {
195
+ const archetypeComponents = getArchetypeComponents(firstArchetype);
196
+ for (const component of archetypeComponents) {
197
+ query = query.with(component);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Handle included components
203
+ if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
204
+ const requireAll = componentTarget.requireAllIncluded ?? true;
205
+ if (requireAll) {
206
+ // ALL included components must be present (AND logic)
207
+ for (const component of componentTarget.includeComponents) {
208
+ query = query.with(component);
209
+ }
210
+ } else {
211
+ // ANY included component must be present (OR logic)
212
+ // For OR logic with Query API, we need to use a different approach
213
+ // This is a simplified implementation - in practice, you might need custom query logic
214
+ for (const component of componentTarget.includeComponents) {
215
+ query = query.with(component);
216
+ break; // Just use the first one for simplicity
217
+ }
218
+ }
219
+ }
220
+
221
+ // Handle excluded components
222
+ if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
223
+ for(const component of componentTarget.excludeComponents){
224
+ query = query.without(component);
225
+ }
226
+ }
227
+
228
+ return query;
229
+ }
230
+
231
+ /**
232
+ * Extract component classes from an ArcheType
233
+ * @param archetype The archetype to extract components from
234
+ * @returns Array of component classes
235
+ */
236
+ export function getArchetypeComponents(archetype: ArcheType): (new () => BaseComponent)[] {
237
+ // Access the private componentMap from ArcheType
238
+ const componentMap = (archetype as any).componentMap as Record<string, new () => BaseComponent>;
239
+ if (!componentMap) {
240
+ return [];
241
+ }
242
+ return Object.values(componentMap);
243
+ }
244
+
245
+ export async function doExecuteTask(manager: SchedulerManager, taskId: string): Promise<void> {
246
+ const taskInfo = manager.tasks.get(taskId);
247
+ if (!taskInfo || !taskInfo.enabled) {
248
+ return;
249
+ }
250
+
251
+ // Skip if the previous tick is still executing. Without this guard
252
+ // a slow task with interval < execution-time burns a lock-acquire
253
+ // round-trip every tick and floods the skipped-executions metric
254
+ // (H-SCHED-1). Cheap in-process check before reaching out to PG.
255
+ if (taskInfo.isRunning) {
256
+ manager.metrics.skippedExecutions++;
257
+ if (manager.config.enableLogging) {
258
+ loggerInstance.debug(`Task ${taskInfo.name} skipped - previous execution still running`);
259
+ }
260
+ return;
261
+ }
262
+
263
+ if (manager.metrics.runningTasks >= manager.config.maxConcurrentTasks) {
264
+ if (manager.config.enableLogging) {
265
+ loggerInstance.warn(`Maximum concurrent tasks reached. Skipping execution of ${taskInfo.name}`);
266
+ }
267
+ return;
268
+ }
269
+
270
+ // Try to acquire distributed lock before executing
271
+ manager.metrics.lockAttempts++;
272
+ const lockResult = await manager.distributedLock.tryAcquire(taskId);
273
+
274
+ if (!lockResult.acquired) {
275
+ // Another instance is executing this task
276
+ manager.metrics.skippedExecutions++;
277
+
278
+ if (manager.config.enableLogging) {
279
+ loggerInstance.debug(`Task ${taskInfo.name} skipped - another instance is executing (lock key: ${lockResult.lockKey})`);
280
+ }
281
+
282
+ manager.emitEvent({
283
+ type: 'task.skipped',
284
+ taskId: taskInfo.id,
285
+ timestamp: new Date(),
286
+ data: { reason: 'lock_unavailable', lockKey: lockResult.lockKey.toString() }
287
+ });
288
+
289
+ return;
290
+ }
291
+
292
+ // Lock acquired successfully
293
+ manager.metrics.locksAcquired++;
294
+
295
+ manager.emitEvent({
296
+ type: 'task.lock.acquired',
297
+ taskId: taskInfo.id,
298
+ timestamp: new Date(),
299
+ data: { lockKey: lockResult.lockKey.toString() }
300
+ });
301
+
302
+ taskInfo.isRunning = true;
303
+ taskInfo.lastExecution = new Date();
304
+ manager.metrics.runningTasks++;
305
+
306
+ const startTime = Date.now();
307
+ const timeout = taskInfo.options?.timeout || manager.config.defaultTimeout;
308
+
309
+ try {
310
+ // Create query based on targeting configuration
311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
312
+ let query: Query<any> | null = null;
313
+
314
+ if (taskInfo.options?.query) {
315
+ // Use custom query function (preferred approach)
316
+ query = taskInfo.options.query();
317
+ } else if (taskInfo.options?.componentTarget) {
318
+ // Use component targeting configuration (deprecated - use query instead)
319
+ const componentTarget = taskInfo.options.componentTarget;
320
+ query = buildQueryFromComponentTarget(componentTarget);
321
+ } else if (taskInfo.componentTarget) {
322
+ // Use legacy single component targeting (deprecated - use query instead)
323
+ query = new Query().with(taskInfo.componentTarget);
324
+ }
325
+ // else: time-based task — no entity selection. Handler invoked
326
+ // with no arguments on each tick.
327
+
328
+ // Apply entity limit if specified (can be used with query function)
329
+ if (query && taskInfo.options?.maxEntitiesPerExecution) {
330
+ query.take(taskInfo.options.maxEntitiesPerExecution);
331
+ }
332
+
333
+ const entities = query ? await query.exec() : [];
334
+
335
+ // Execute the scheduled method with the entities array
336
+ const method = taskInfo.service[taskInfo.methodName];
337
+ if (typeof method !== 'function') {
338
+ throw new Error(`Method ${taskInfo.methodName} not found on service`);
339
+ }
340
+
341
+ // Execute with timeout. Time-based tasks receive no entity arg.
342
+ const result = await executeWithTimeout(
343
+ manager,
344
+ query
345
+ ? method.call(taskInfo.service, entities)
346
+ : method.call(taskInfo.service),
347
+ timeout,
348
+ taskInfo
349
+ );
350
+
351
+ const duration = Date.now() - startTime;
352
+ taskInfo.executionCount++;
353
+ manager.metrics.completedExecutions++;
354
+ manager.metrics.totalExecutionTime += duration;
355
+ manager.metrics.averageExecutionTime = manager.metrics.totalExecutionTime / manager.metrics.completedExecutions;
356
+
357
+ // Update task-specific metrics
358
+ updateTaskMetrics(manager, taskInfo.id, {
359
+ totalExecutions: taskInfo.executionCount,
360
+ successfulExecutions: (manager.metrics.taskMetrics[taskInfo.id]?.successfulExecutions || 0) + 1,
361
+ averageExecutionTime: duration,
362
+ lastExecutionTime: new Date(),
363
+ totalEntitiesProcessed: entities.length
364
+ });
365
+
366
+ if (manager.config.enableLogging) {
367
+ loggerInstance.info(`Task ${taskInfo.name} completed successfully in ${duration}ms (processed ${entities.length} entities)`);
368
+ }
369
+
370
+ manager.emitEvent({
371
+ type: 'task.executed',
372
+ taskId: taskInfo.id,
373
+ timestamp: new Date(),
374
+ data: { duration, entitiesProcessed: entities.length, success: true }
375
+ });
376
+
377
+ } catch (error) {
378
+ const duration = Date.now() - startTime;
379
+ manager.metrics.failedExecutions++;
380
+
381
+ // Handle retry logic
382
+ await handleTaskFailure(manager, taskInfo, error instanceof Error ? error : new Error(String(error)), duration);
383
+
384
+ if (manager.config.enableLogging) {
385
+ loggerInstance.error(`Task ${taskInfo.name} failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
386
+ }
387
+
388
+ manager.emitEvent({
389
+ type: 'task.failed',
390
+ taskId: taskInfo.id,
391
+ timestamp: new Date(),
392
+ data: { duration, error: error instanceof Error ? error.message : String(error) }
393
+ });
394
+
395
+ } finally {
396
+ taskInfo.isRunning = false;
397
+ manager.metrics.runningTasks--;
398
+
399
+ // Release the distributed lock
400
+ await manager.distributedLock.release(taskId);
401
+
402
+ manager.emitEvent({
403
+ type: 'task.lock.released',
404
+ taskId: taskInfo.id,
405
+ timestamp: new Date(),
406
+ data: { lockKey: lockResult.lockKey.toString() }
407
+ });
408
+ }
409
+ }
410
+
411
+ export async function executeTask(manager: SchedulerManager, taskId: string): Promise<void> {
412
+ // Track this execution so stop() can await in-flight work before
413
+ // resources (DB pool, cache) are torn down. Without this, a task mid-
414
+ // write during SIGTERM hits a closed DB pool and silently corrupts
415
+ // or loses data.
416
+ const p = doExecuteTask(manager, taskId);
417
+ manager.inflightTasks.add(p);
418
+ p.finally(() => manager.inflightTasks.delete(p));
419
+ return p;
420
+ }