bunsane 0.1.0 → 0.1.1

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 (85) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +119 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +159 -12
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +453 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +65 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +1 -1
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/examples/hooks/README.md +228 -0
  52. package/examples/hooks/audit-logger.ts +495 -0
  53. package/gql/Generator.ts +56 -34
  54. package/gql/decorators/Upload.ts +176 -0
  55. package/gql/helpers.ts +67 -0
  56. package/gql/index.ts +55 -31
  57. package/gql/types.ts +1 -1
  58. package/index.ts +79 -11
  59. package/package.json +5 -4
  60. package/rest/Generator.ts +3 -0
  61. package/rest/index.ts +22 -0
  62. package/service/Service.ts +1 -1
  63. package/service/ServiceRegistry.ts +10 -6
  64. package/service/index.ts +12 -1
  65. package/tests/bench/insert.bench.ts +59 -0
  66. package/tests/bench/relations.bench.ts +269 -0
  67. package/tests/bench/sorting.bench.ts +415 -0
  68. package/tests/component-hooks.test.ts +1409 -0
  69. package/tests/component.test.ts +205 -0
  70. package/tests/errorHandling.test.ts +155 -0
  71. package/tests/hooks.test.ts +666 -0
  72. package/tests/query-sorting.test.ts +101 -0
  73. package/tests/relations.test.ts +169 -0
  74. package/tests/scheduler.test.ts +724 -0
  75. package/tsconfig.json +35 -34
  76. package/types/graphql.types.ts +87 -0
  77. package/types/hooks.types.ts +141 -0
  78. package/types/scheduler.types.ts +165 -0
  79. package/types/upload.types.ts +184 -0
  80. package/upload/index.ts +140 -0
  81. package/utils/UploadHelper.ts +305 -0
  82. package/utils/cronParser.ts +366 -0
  83. package/utils/errorMessages.ts +151 -0
  84. package/validate-docs.sh +90 -0
  85. package/core/Events.ts +0 -0
@@ -0,0 +1,710 @@
1
+ import { logger } from "./Logger";
2
+ import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
3
+ import {
4
+ ScheduleInterval
5
+ } from "../types/scheduler.types";
6
+ import type {
7
+ ScheduledTaskInfo,
8
+ SchedulerMetrics,
9
+ TaskExecutionResult,
10
+ SchedulerEvent,
11
+ SchedulerEventCallback,
12
+ SchedulerConfig,
13
+ TaskMetrics
14
+ } from "../types/scheduler.types";
15
+ import Query from "./Query";
16
+ import { Entity } from "./Entity";
17
+ import { CronParser } from "../utils/cronParser";
18
+ import type { ComponentTargetConfig } from "./EntityHookManager";
19
+ import ArcheType from "./ArcheType";
20
+ import { BaseComponent } from "./Components";
21
+
22
+ const loggerInstance = logger.child({ scope: "SchedulerManager" });
23
+
24
+ export class SchedulerManager {
25
+ private static instance: SchedulerManager;
26
+ private tasks: Map<string, ScheduledTaskInfo> = new Map();
27
+ private intervals: Map<string, NodeJS.Timeout> = new Map();
28
+ private isRunning: boolean = false;
29
+ private eventListeners: SchedulerEventCallback[] = [];
30
+ private config: SchedulerConfig;
31
+ private metrics: SchedulerMetrics = {
32
+ totalTasks: 0,
33
+ runningTasks: 0,
34
+ completedExecutions: 0,
35
+ failedExecutions: 0,
36
+ averageExecutionTime: 0,
37
+ totalExecutionTime: 0,
38
+ timedOutTasks: 0,
39
+ retriedTasks: 0,
40
+ taskMetrics: {}
41
+ };
42
+
43
+ private constructor() {
44
+ this.config = {
45
+ enabled: true,
46
+ maxConcurrentTasks: 5,
47
+ defaultTimeout: 30000, // 30 seconds
48
+ enableLogging: true,
49
+ runOnStart: true
50
+ };
51
+
52
+ this.initializeLifecycleIntegration();
53
+ }
54
+
55
+ public static getInstance(): SchedulerManager {
56
+ if (!SchedulerManager.instance) {
57
+ SchedulerManager.instance = new SchedulerManager();
58
+ }
59
+ return SchedulerManager.instance;
60
+ }
61
+
62
+ private initializeLifecycleIntegration(): void {
63
+ ApplicationLifecycle.addPhaseListener((event) => {
64
+ const phase = event.detail;
65
+ if (phase === ApplicationPhase.APPLICATION_READY) {
66
+ logger.info("Scheduler initialized and ready");
67
+ if (this.config.runOnStart) {
68
+ this.start();
69
+ }
70
+ }
71
+ });
72
+ }
73
+
74
+ public registerTask(taskInfo: ScheduledTaskInfo): void {
75
+ if (this.tasks.has(taskInfo.id)) {
76
+ loggerInstance.warn(`Task ${taskInfo.id} is already registered. Skipping registration.`);
77
+ return;
78
+ }
79
+
80
+ // 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)`);
83
+ loggerInstance.error(`Failed to register task: ${error.message}`);
84
+ throw error;
85
+ }
86
+
87
+ if (!taskInfo.service) {
88
+ const error = new Error(`Task ${taskInfo.id} has no service instance`);
89
+ loggerInstance.error(`Failed to register task: ${error.message}`);
90
+ throw error;
91
+ }
92
+
93
+ if (typeof taskInfo.service[taskInfo.methodName] !== 'function') {
94
+ const error = new Error(`Method ${taskInfo.methodName} not found on service for task ${taskInfo.id}`);
95
+ loggerInstance.error(`Failed to register task: ${error.message}`);
96
+ throw error;
97
+ }
98
+
99
+ // Try to schedule the task - if scheduling fails, don't register it
100
+ try {
101
+ this.scheduleTask(taskInfo);
102
+ this.tasks.set(taskInfo.id, taskInfo);
103
+ this.metrics.totalTasks++;
104
+
105
+ if (this.config.enableLogging) {
106
+ loggerInstance.info(`Registered scheduled task: ${taskInfo.name} (${taskInfo.id})`);
107
+ }
108
+
109
+ this.emitEvent({
110
+ type: 'task.registered',
111
+ taskId: taskInfo.id,
112
+ timestamp: new Date(),
113
+ data: { taskName: taskInfo.name, interval: taskInfo.interval }
114
+ });
115
+ } catch (error) {
116
+ loggerInstance.error(`Failed to schedule task ${taskInfo.name}, not registering: ${error instanceof Error ? error.message : String(error)}`);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ private scheduleTask(taskInfo: ScheduledTaskInfo): void {
122
+ try {
123
+ if (taskInfo.interval === ScheduleInterval.CRON) {
124
+ this.scheduleCronTask(taskInfo);
125
+ } else {
126
+ this.scheduleIntervalTask(taskInfo);
127
+ }
128
+ } catch (error) {
129
+ loggerInstance.error(`Failed to schedule task ${taskInfo.name}: ${error instanceof Error ? error.message : String(error)}`);
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ private scheduleIntervalTask(taskInfo: ScheduledTaskInfo): void {
135
+ const intervalMs = this.getIntervalMilliseconds(taskInfo.interval);
136
+
137
+ // For very long intervals (monthly), use a different approach
138
+ if (intervalMs > 24 * 60 * 60 * 1000) { // More than 24 hours
139
+ this.scheduleLongIntervalTask(taskInfo, intervalMs);
140
+ } else {
141
+ const intervalId = setInterval(async () => {
142
+ await this.executeTask(taskInfo.id);
143
+ }, intervalMs);
144
+
145
+ this.intervals.set(taskInfo.id, intervalId);
146
+ taskInfo.nextExecution = new Date(Date.now() + intervalMs);
147
+ }
148
+
149
+ if (this.config.enableLogging) {
150
+ loggerInstance.info(`Scheduled task ${taskInfo.name} to run every ${intervalMs}ms`);
151
+ }
152
+ }
153
+
154
+ private scheduleLongIntervalTask(taskInfo: ScheduledTaskInfo, intervalMs: number): void {
155
+ // For very long intervals, use a shorter check interval to avoid timeout overflow
156
+ const checkInterval = Math.min(intervalMs, 24 * 60 * 60 * 1000); // Max 24 hours check interval
157
+ const nextExecution = new Date(Date.now() + intervalMs);
158
+ taskInfo.nextExecution = nextExecution;
159
+
160
+ const intervalId = setInterval(async () => {
161
+ const now = Date.now();
162
+ if (now >= nextExecution.getTime()) {
163
+ await this.executeTask(taskInfo.id);
164
+ // Reschedule for next execution
165
+ taskInfo.nextExecution = new Date(now + intervalMs);
166
+ }
167
+ }, checkInterval);
168
+
169
+ this.intervals.set(taskInfo.id, intervalId);
170
+ }
171
+
172
+ private scheduleCronTask(taskInfo: ScheduledTaskInfo): void {
173
+ if (!taskInfo.cronExpression) {
174
+ throw new Error(`Cron expression is required for CRON interval tasks`);
175
+ }
176
+
177
+ // Validate cron expression
178
+ const validation = CronParser.validate(taskInfo.cronExpression);
179
+ if (!validation.isValid) {
180
+ throw new Error(`Invalid cron expression: ${validation.error}`);
181
+ }
182
+
183
+ // Calculate next execution time
184
+ const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
185
+ if (!nextExecution) {
186
+ throw new Error(`Unable to calculate next execution time for cron expression: ${taskInfo.cronExpression}`);
187
+ }
188
+
189
+ taskInfo.nextExecution = nextExecution;
190
+
191
+ // Schedule the task to run at the calculated time
192
+ const timeoutId = setTimeout(async () => {
193
+ await this.executeTask(taskInfo.id);
194
+ // Reschedule for next execution
195
+ this.scheduleCronTask(taskInfo);
196
+ }, nextExecution.getTime() - Date.now());
197
+
198
+ this.intervals.set(taskInfo.id, timeoutId as any);
199
+
200
+ if (this.config.enableLogging) {
201
+ loggerInstance.info(`Scheduled cron task ${taskInfo.name} to run at ${nextExecution.toISOString()}`);
202
+ }
203
+ }
204
+
205
+ private getIntervalMilliseconds(interval: ScheduleInterval): number {
206
+ switch (interval) {
207
+ case ScheduleInterval.MINUTE:
208
+ return 60 * 1000; // 1 minute
209
+ case ScheduleInterval.HOUR:
210
+ return 60 * 60 * 1000; // 1 hour
211
+ case ScheduleInterval.DAILY:
212
+ return 24 * 60 * 60 * 1000; // 24 hours
213
+ case ScheduleInterval.WEEKLY:
214
+ return 7 * 24 * 60 * 60 * 1000; // 7 days
215
+ case ScheduleInterval.MONTHLY:
216
+ return 30 * 24 * 60 * 60 * 1000; // 30 days (approximate)
217
+ default:
218
+ throw new Error(`Unsupported interval: ${interval}`);
219
+ }
220
+ }
221
+
222
+ private async executeTask(taskId: string): Promise<void> {
223
+ const taskInfo = this.tasks.get(taskId);
224
+ if (!taskInfo || !taskInfo.enabled) {
225
+ return;
226
+ }
227
+
228
+ if (this.metrics.runningTasks >= this.config.maxConcurrentTasks) {
229
+ if (this.config.enableLogging) {
230
+ loggerInstance.warn(`Maximum concurrent tasks reached. Skipping execution of ${taskInfo.name}`);
231
+ }
232
+ return;
233
+ }
234
+
235
+ taskInfo.isRunning = true;
236
+ taskInfo.lastExecution = new Date();
237
+ this.metrics.runningTasks++;
238
+
239
+ const startTime = Date.now();
240
+ const timeout = taskInfo.options?.timeout || this.config.defaultTimeout;
241
+
242
+ 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
248
+ const componentTarget = taskInfo.options.componentTarget;
249
+ query = this.buildQueryFromComponentTarget(componentTarget);
250
+ } else if (taskInfo.componentTarget) {
251
+ // Use legacy single component targeting
252
+ query = new Query().with(taskInfo.componentTarget);
253
+ } else {
254
+ throw new Error('No component target specified');
255
+ }
256
+
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
288
+ if (taskInfo.options?.maxEntitiesPerExecution) {
289
+ query.take(taskInfo.options.maxEntitiesPerExecution);
290
+ }
291
+
292
+ const entities = await query.exec();
293
+
294
+ // Execute the scheduled method with the entities array
295
+ const method = taskInfo.service[taskInfo.methodName];
296
+ if (typeof method !== 'function') {
297
+ throw new Error(`Method ${taskInfo.methodName} not found on service`);
298
+ }
299
+
300
+ // Execute with timeout
301
+ const result = await this.executeWithTimeout(
302
+ method.call(taskInfo.service, entities),
303
+ timeout,
304
+ taskInfo
305
+ );
306
+
307
+ const duration = Date.now() - startTime;
308
+ taskInfo.executionCount++;
309
+ this.metrics.completedExecutions++;
310
+ this.metrics.totalExecutionTime += duration;
311
+ this.metrics.averageExecutionTime = this.metrics.totalExecutionTime / this.metrics.completedExecutions;
312
+
313
+ // Update task-specific metrics
314
+ this.updateTaskMetrics(taskInfo.id, {
315
+ totalExecutions: taskInfo.executionCount,
316
+ successfulExecutions: (this.metrics.taskMetrics[taskInfo.id]?.successfulExecutions || 0) + 1,
317
+ averageExecutionTime: duration,
318
+ lastExecutionTime: new Date(),
319
+ totalEntitiesProcessed: entities.length
320
+ });
321
+
322
+ if (this.config.enableLogging) {
323
+ loggerInstance.info(`Task ${taskInfo.name} completed successfully in ${duration}ms (processed ${entities.length} entities)`);
324
+ }
325
+
326
+ this.emitEvent({
327
+ type: 'task.executed',
328
+ taskId: taskInfo.id,
329
+ timestamp: new Date(),
330
+ data: { duration, entitiesProcessed: entities.length, success: true }
331
+ });
332
+
333
+ } catch (error) {
334
+ const duration = Date.now() - startTime;
335
+ this.metrics.failedExecutions++;
336
+
337
+ // Handle retry logic
338
+ await this.handleTaskFailure(taskInfo, error instanceof Error ? error : new Error(String(error)), duration);
339
+
340
+ if (this.config.enableLogging) {
341
+ loggerInstance.error(`Task ${taskInfo.name} failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
342
+ }
343
+
344
+ this.emitEvent({
345
+ type: 'task.failed',
346
+ taskId: taskInfo.id,
347
+ timestamp: new Date(),
348
+ data: { duration, error: error instanceof Error ? error.message : String(error) }
349
+ });
350
+
351
+ } finally {
352
+ taskInfo.isRunning = false;
353
+ this.metrics.runningTasks--;
354
+ }
355
+ }
356
+
357
+ public start(): void {
358
+ if (this.isRunning) {
359
+ loggerInstance.warn("Scheduler is already running");
360
+ return;
361
+ }
362
+
363
+ this.isRunning = true;
364
+
365
+ // Sort tasks by priority before scheduling (higher priority first)
366
+ const sortedTasks = Array.from(this.tasks.values())
367
+ .filter(task => task.enabled)
368
+ .sort((a, b) => {
369
+ const priorityA = a.options?.priority ?? a.priority ?? 0;
370
+ const priorityB = b.options?.priority ?? b.priority ?? 0;
371
+ return priorityB - priorityA; // Higher priority first
372
+ });
373
+
374
+ // Schedule all registered tasks in priority order
375
+ for (const taskInfo of sortedTasks) {
376
+ this.scheduleTask(taskInfo);
377
+ }
378
+
379
+ if (this.config.enableLogging) {
380
+ loggerInstance.info(`Scheduler started with ${this.tasks.size} tasks (sorted by priority)`);
381
+ }
382
+
383
+ this.emitEvent({
384
+ type: 'scheduler.started',
385
+ timestamp: new Date(),
386
+ data: { taskCount: this.tasks.size }
387
+ });
388
+ }
389
+
390
+ public stop(): void {
391
+ if (!this.isRunning) {
392
+ loggerInstance.warn("Scheduler is not running");
393
+ return;
394
+ }
395
+
396
+ this.isRunning = false;
397
+
398
+ // Clear all intervals and timeouts
399
+ for (const intervalId of this.intervals.values()) {
400
+ clearInterval(intervalId);
401
+ clearTimeout(intervalId as any);
402
+ }
403
+ this.intervals.clear();
404
+
405
+ if (this.config.enableLogging) {
406
+ loggerInstance.info("Scheduler stopped");
407
+ }
408
+
409
+ this.emitEvent({
410
+ type: 'scheduler.stopped',
411
+ timestamp: new Date()
412
+ });
413
+ }
414
+
415
+ public getMetrics(): SchedulerMetrics {
416
+ return { ...this.metrics };
417
+ }
418
+
419
+ public getTasks(): ScheduledTaskInfo[] {
420
+ return Array.from(this.tasks.values()).map(task => ({ ...task }));
421
+ }
422
+
423
+ public enableTask(taskId: string): boolean {
424
+ const task = this.tasks.get(taskId);
425
+ if (!task) {
426
+ return false;
427
+ }
428
+
429
+ task.enabled = true;
430
+ if (this.isRunning) {
431
+ this.scheduleTask(task);
432
+ }
433
+ return true;
434
+ }
435
+
436
+ public disableTask(taskId: string): boolean {
437
+ const task = this.tasks.get(taskId);
438
+ if (!task) {
439
+ return false;
440
+ }
441
+
442
+ task.enabled = false;
443
+ const intervalId = this.intervals.get(taskId);
444
+ if (intervalId) {
445
+ clearInterval(intervalId);
446
+ clearTimeout(intervalId as any);
447
+ this.intervals.delete(taskId);
448
+ }
449
+ return true;
450
+ }
451
+
452
+ public addEventListener(callback: SchedulerEventCallback): void {
453
+ this.eventListeners.push(callback);
454
+ }
455
+
456
+ public removeEventListener(callback: SchedulerEventCallback): void {
457
+ const index = this.eventListeners.indexOf(callback);
458
+ if (index > -1) {
459
+ this.eventListeners.splice(index, 1);
460
+ }
461
+ }
462
+
463
+ private emitEvent(event: SchedulerEvent): void {
464
+ for (const listener of this.eventListeners) {
465
+ try {
466
+ listener(event);
467
+ } catch (error) {
468
+ loggerInstance.error(`Error in scheduler event listener: ${error instanceof Error ? error.message : String(error)}`);
469
+ }
470
+ }
471
+ }
472
+
473
+ public updateConfig(config: Partial<SchedulerConfig>): void {
474
+ this.config = { ...this.config, ...config };
475
+ if (this.config.enableLogging) {
476
+ loggerInstance.info(`Scheduler configuration updated: ${JSON.stringify(config)}`);
477
+ }
478
+ }
479
+
480
+ public getConfig(): SchedulerConfig {
481
+ return { ...this.config };
482
+ }
483
+
484
+ /**
485
+ * Execute a task with timeout enforcement
486
+ */
487
+ private async executeWithTimeout<T>(task: Promise<T>, timeoutMs: number, taskInfo: ScheduledTaskInfo): Promise<T> {
488
+ return new Promise((resolve, reject) => {
489
+ const timeoutId = setTimeout(() => {
490
+ clearTimeout(timeoutId);
491
+ this.metrics.timedOutTasks++;
492
+ this.updateTaskMetrics(taskInfo.id, {
493
+ timeoutCount: (this.metrics.taskMetrics[taskInfo.id]?.timeoutCount || 0) + 1
494
+ });
495
+ const error = new Error(`Task ${taskInfo.name} timed out after ${timeoutMs}ms`);
496
+ this.emitEvent({
497
+ type: 'task.timeout',
498
+ taskId: taskInfo.id,
499
+ timestamp: new Date(),
500
+ data: { timeoutMs, taskName: taskInfo.name }
501
+ });
502
+ reject(error);
503
+ }, timeoutMs);
504
+
505
+ task
506
+ .then((result) => {
507
+ clearTimeout(timeoutId);
508
+ resolve(result);
509
+ })
510
+ .catch((error) => {
511
+ clearTimeout(timeoutId);
512
+ reject(error);
513
+ });
514
+ });
515
+ }
516
+
517
+ /**
518
+ * Handle task failure with retry logic
519
+ */
520
+ private async handleTaskFailure(taskInfo: ScheduledTaskInfo, error: Error, duration: number): Promise<void> {
521
+ taskInfo.lastError = error.message;
522
+
523
+ const maxRetries = taskInfo.options?.maxRetries || taskInfo.maxRetries || 0;
524
+ const retryDelay = taskInfo.options?.retryDelay || 1000; // Default 1 second
525
+
526
+ if (taskInfo.retryCount === undefined) {
527
+ taskInfo.retryCount = 0;
528
+ }
529
+
530
+ if (taskInfo.retryCount < maxRetries) {
531
+ taskInfo.retryCount++;
532
+ this.metrics.retriedTasks++;
533
+
534
+ this.updateTaskMetrics(taskInfo.id, {
535
+ retryCount: taskInfo.retryCount
536
+ });
537
+
538
+ if (this.config.enableLogging) {
539
+ loggerInstance.warn(`Task ${taskInfo.name} failed (attempt ${taskInfo.retryCount}/${maxRetries}), retrying in ${retryDelay}ms: ${error.message}`);
540
+ }
541
+
542
+ // Schedule retry
543
+ setTimeout(async () => {
544
+ await this.executeTask(taskInfo.id);
545
+ }, retryDelay);
546
+
547
+ this.emitEvent({
548
+ type: 'task.retry',
549
+ taskId: taskInfo.id,
550
+ timestamp: new Date(),
551
+ data: {
552
+ attempt: taskInfo.retryCount,
553
+ maxRetries,
554
+ retryDelay,
555
+ error: error.message
556
+ }
557
+ });
558
+ } else {
559
+ // Max retries reached or no retries configured
560
+ this.updateTaskMetrics(taskInfo.id, {
561
+ failedExecutions: (this.metrics.taskMetrics[taskInfo.id]?.failedExecutions || 0) + 1
562
+ });
563
+
564
+ if (this.config.enableLogging) {
565
+ loggerInstance.error(`Task ${taskInfo.name} failed permanently after ${taskInfo.retryCount} attempts: ${error.message}`);
566
+ }
567
+
568
+ this.emitEvent({
569
+ type: 'task.failed',
570
+ taskId: taskInfo.id,
571
+ timestamp: new Date(),
572
+ data: {
573
+ duration,
574
+ error: error.message,
575
+ attempts: taskInfo.retryCount,
576
+ maxRetries
577
+ }
578
+ });
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Update task-specific metrics
584
+ */
585
+ private updateTaskMetrics(taskId: string, updates: Partial<TaskMetrics>): void {
586
+ if (!this.metrics.taskMetrics[taskId]) {
587
+ const taskInfo = this.tasks.get(taskId);
588
+ this.metrics.taskMetrics[taskId] = {
589
+ taskId,
590
+ taskName: taskInfo?.name || 'Unknown',
591
+ totalExecutions: 0,
592
+ successfulExecutions: 0,
593
+ failedExecutions: 0,
594
+ averageExecutionTime: 0,
595
+ totalEntitiesProcessed: 0,
596
+ retryCount: 0,
597
+ timeoutCount: 0
598
+ };
599
+ }
600
+
601
+ const metrics = this.metrics.taskMetrics[taskId];
602
+ Object.assign(metrics, updates);
603
+
604
+ // Update rolling averages
605
+ if (updates.averageExecutionTime !== undefined) {
606
+ const currentAvg = metrics.averageExecutionTime;
607
+ const newCount = metrics.totalExecutions;
608
+ metrics.averageExecutionTime = ((currentAvg * (newCount - 1)) + updates.averageExecutionTime) / newCount;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Get detailed metrics for a specific task
614
+ */
615
+ public getTaskMetrics(taskId: string): TaskMetrics | null {
616
+ return this.metrics.taskMetrics[taskId] || null;
617
+ }
618
+
619
+ /**
620
+ * Get all task metrics
621
+ */
622
+ public getAllTaskMetrics(): Record<string, TaskMetrics> {
623
+ return { ...this.metrics.taskMetrics };
624
+ }
625
+
626
+ /**
627
+ * Manually execute a task for testing purposes
628
+ * @param taskId The ID of the task to execute
629
+ */
630
+ public async executeTaskNow(taskId: string): Promise<boolean> {
631
+ const taskInfo = this.tasks.get(taskId);
632
+ if (!taskInfo || !taskInfo.enabled) {
633
+ return false;
634
+ }
635
+
636
+ await this.executeTask(taskId);
637
+ return true;
638
+ }
639
+
640
+ /**
641
+ * Build a Query object from ComponentTargetConfig
642
+ * @param componentTarget The component targeting configuration
643
+ * @returns A Query object configured with the component targeting
644
+ */
645
+ private buildQueryFromComponentTarget(componentTarget: ComponentTargetConfig): Query {
646
+ let query = new Query();
647
+
648
+ // Handle archetype matching first (most specific)
649
+ if (componentTarget.archetype) {
650
+ // For archetype matching, we need to include all components from the archetype
651
+ const archetypeComponents = this.getArchetypeComponents(componentTarget.archetype);
652
+ for (const component of archetypeComponents) {
653
+ query = query.with(component);
654
+ }
655
+ } else if (componentTarget.archetypes && componentTarget.archetypes.length > 0) {
656
+ // Handle multiple archetypes - for simplicity, we'll use the first valid one
657
+ // In a more advanced implementation, you might want to handle OR logic
658
+ const firstArchetype = componentTarget.archetypes.find(archetype => archetype !== undefined);
659
+ if (firstArchetype) {
660
+ const archetypeComponents = this.getArchetypeComponents(firstArchetype);
661
+ for (const component of archetypeComponents) {
662
+ query = query.with(component);
663
+ }
664
+ }
665
+ }
666
+
667
+ // Handle included components
668
+ if (componentTarget.includeComponents && componentTarget.includeComponents.length > 0) {
669
+ const requireAll = componentTarget.requireAllIncluded ?? true;
670
+ if (requireAll) {
671
+ // ALL included components must be present (AND logic)
672
+ for (const component of componentTarget.includeComponents) {
673
+ query = query.with(component);
674
+ }
675
+ } else {
676
+ // ANY included component must be present (OR logic)
677
+ // For OR logic with Query API, we need to use a different approach
678
+ // This is a simplified implementation - in practice, you might need custom query logic
679
+ for (const component of componentTarget.includeComponents) {
680
+ query = query.with(component);
681
+ break; // Just use the first one for simplicity
682
+ }
683
+ }
684
+ }
685
+
686
+ // Handle excluded components
687
+ if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
688
+ // Note: The current Query API might not directly support exclusion
689
+ // This would require extending the Query API or using post-filtering
690
+ // For now, we'll log a warning and continue
691
+ loggerInstance.warn('excludeComponents is not fully supported in scheduled tasks yet. Consider using post-query filtering.');
692
+ }
693
+
694
+ return query;
695
+ }
696
+
697
+ /**
698
+ * Extract component classes from an ArcheType
699
+ * @param archetype The archetype to extract components from
700
+ * @returns Array of component classes
701
+ */
702
+ private getArchetypeComponents(archetype: ArcheType): (new () => BaseComponent)[] {
703
+ // Access the private componentMap from ArcheType
704
+ const componentMap = (archetype as any).componentMap as Record<string, new () => BaseComponent>;
705
+ if (!componentMap) {
706
+ return [];
707
+ }
708
+ return Object.values(componentMap);
709
+ }
710
+ }