@memberjunction/server 2.105.0 → 2.107.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/src/index.ts CHANGED
@@ -40,6 +40,12 @@ LoadCoreEntitiesServerSubClasses(); // prevent tree shaking for this dynamic mod
40
40
  import { LoadAgentManagementActions } from '@memberjunction/ai-agent-manager-actions';
41
41
  LoadAgentManagementActions();
42
42
 
43
+ import { LoadSchedulingEngine } from '@memberjunction/scheduling-engine';
44
+ LoadSchedulingEngine(); // This also loads drivers
45
+
46
+ import { LoadAllSchedulingActions } from '@memberjunction/scheduling-actions';
47
+ LoadAllSchedulingActions(); // prevent tree shaking for scheduling actions
48
+
43
49
 
44
50
  import { resolve } from 'node:path';
45
51
  import { DataSourceInfo, raiseEvent } from './types.js';
@@ -50,6 +56,7 @@ LoadAIEngine();
50
56
  LoadAIProviders();
51
57
 
52
58
  import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
59
+ import { ScheduledJobsService } from './services/ScheduledJobsService.js';
53
60
 
54
61
  const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
55
62
 
@@ -302,6 +309,19 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
302
309
  // Set up REST endpoints with the configured options and auth middleware
303
310
  setupRESTEndpoints(app, restApiConfig, authMiddleware);
304
311
 
312
+ // Initialize and start scheduled jobs service if enabled
313
+ let scheduledJobsService: ScheduledJobsService | null = null;
314
+ if (configInfo.scheduledJobs?.enabled) {
315
+ try {
316
+ scheduledJobsService = new ScheduledJobsService(configInfo.scheduledJobs);
317
+ await scheduledJobsService.Initialize();
318
+ await scheduledJobsService.Start();
319
+ } catch (error) {
320
+ console.error('❌ Failed to start scheduled jobs service:', error);
321
+ // Don't throw - allow server to start even if scheduled jobs fail
322
+ }
323
+ }
324
+
305
325
  if (options?.onBeforeServe) {
306
326
  await Promise.resolve(options.onBeforeServe());
307
327
  }
@@ -309,4 +329,34 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
309
329
  await new Promise<void>((resolve) => httpServer.listen({ port: graphqlPort }, resolve));
310
330
  console.log(`📦 Connected to database: ${dbHost}:${dbPort}/${dbDatabase}`);
311
331
  console.log(`🚀 Server ready at http://localhost:${graphqlPort}/`);
332
+
333
+ // Set up graceful shutdown handlers
334
+ const gracefulShutdown = async (signal: string) => {
335
+ console.log(`\n${signal} received, shutting down gracefully...`);
336
+
337
+ // Stop scheduled jobs service
338
+ if (scheduledJobsService?.IsRunning) {
339
+ try {
340
+ await scheduledJobsService.Stop();
341
+ console.log('✅ Scheduled jobs service stopped');
342
+ } catch (error) {
343
+ console.error('❌ Error stopping scheduled jobs service:', error);
344
+ }
345
+ }
346
+
347
+ // Close server
348
+ httpServer.close(() => {
349
+ console.log('✅ HTTP server closed');
350
+ process.exit(0);
351
+ });
352
+
353
+ // Force close after 10 seconds
354
+ setTimeout(() => {
355
+ console.error('⚠️ Forced shutdown after timeout');
356
+ process.exit(1);
357
+ }, 10000);
358
+ };
359
+
360
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
361
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
312
362
  };
@@ -375,7 +375,8 @@ function initializeSkipLearningCycleScheduler() {
375
375
  return;
376
376
  }
377
377
  if (!skipConfigInfo.learningCycleEnabled) {
378
- LogStatus('Skip AI Learning Cycles not enabled in configuration');
378
+ // Skip AI Learning Cycles not enabled - disabled logging to reduce startup noise
379
+ // LogStatus('Skip AI Learning Cycles not enabled in configuration');
379
380
  return;
380
381
  }
381
382
 
@@ -428,8 +429,9 @@ function initializeSkipLearningCycleScheduler() {
428
429
  LogError(`Failed to initialize Skip learning cycle scheduler: ${error}`);
429
430
  }
430
431
  }
431
- // now call the function to initialize the scheduler
432
- initializeSkipLearningCycleScheduler();
432
+ // Disabled: Skip AI Learning Cycles no longer used - commented out to prevent startup initialization
433
+ // If needed in the future, uncomment the line below:
434
+ // initializeSkipLearningCycleScheduler();
433
435
 
434
436
  /**
435
437
  * Base type for Skip API requests containing common fields
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @fileoverview Service module for managing scheduled job lifecycle
3
+ * @module MJServer/services
4
+ */
5
+
6
+ import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
7
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
8
+ import { SchedulingEngine } from '@memberjunction/scheduling-engine';
9
+ import { ScheduledJobsConfig } from '../config.js';
10
+
11
+ /**
12
+ * Service for managing scheduled jobs lifecycle
13
+ * Handles initialization, starting/stopping polling, and graceful shutdown
14
+ */
15
+ export class ScheduledJobsService {
16
+ private engine: SchedulingEngine;
17
+ private systemUser: UserInfo | null = null;
18
+ private isRunning: boolean = false;
19
+ private config: ScheduledJobsConfig;
20
+
21
+ constructor(config: ScheduledJobsConfig) {
22
+ this.config = config;
23
+ this.engine = SchedulingEngine.Instance;
24
+ }
25
+
26
+ /**
27
+ * Initialize the scheduled jobs service
28
+ * Loads metadata and prepares the engine
29
+ */
30
+ public async Initialize(): Promise<void> {
31
+ if (!this.config.enabled) {
32
+ LogStatus('[ScheduledJobsService] Scheduled jobs are disabled in configuration');
33
+ return;
34
+ }
35
+
36
+ try {
37
+ // Get system user for job execution
38
+ this.systemUser = await this.getSystemUser();
39
+
40
+ if (!this.systemUser) {
41
+ throw new Error(`System user not found with email: ${this.config.systemUserEmail}`);
42
+ }
43
+
44
+ // Pre-load metadata cache
45
+ await this.engine.Config(false, this.systemUser);
46
+ } catch (error) {
47
+ LogError('[ScheduledJobsService] Failed to initialize', undefined, error);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Start the scheduled jobs polling
54
+ */
55
+ public async Start(): Promise<void> {
56
+ if (!this.config.enabled) {
57
+ return;
58
+ }
59
+
60
+ if (this.isRunning) {
61
+ LogStatus('[ScheduledJobsService] Already running');
62
+ return;
63
+ }
64
+
65
+ if (!this.systemUser) {
66
+ throw new Error('Service not initialized - call Initialize() first');
67
+ }
68
+
69
+ try {
70
+ this.engine.StartPolling(this.systemUser);
71
+ this.isRunning = true;
72
+
73
+ // Single consolidated console message
74
+ const jobCount = this.engine.ScheduledJobs.length;
75
+ if (jobCount === 0) {
76
+ console.log(`📅 Scheduled Jobs: No active jobs, polling suspended (will auto-start when jobs are added)`);
77
+ } else {
78
+ const interval = this.engine.ActivePollingInterval;
79
+ if (interval !== null) {
80
+ const intervalDisplay = interval >= 60000
81
+ ? `${Math.round(interval / 60000)} minute(s)`
82
+ : `${Math.round(interval / 1000)} second(s)`;
83
+ console.log(`📅 Scheduled Jobs: ${jobCount} active job(s), polling every ${intervalDisplay}`);
84
+ } else {
85
+ // This shouldn't happen if jobCount > 0, but handle it gracefully
86
+ console.log(`📅 Scheduled Jobs: ${jobCount} active job(s), polling interval not set`);
87
+ }
88
+ }
89
+ } catch (error) {
90
+ LogError('[ScheduledJobsService] Failed to start polling', undefined, error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Stop the scheduled jobs polling gracefully
97
+ */
98
+ public async Stop(): Promise<void> {
99
+ if (!this.isRunning) {
100
+ return;
101
+ }
102
+
103
+ try {
104
+ LogStatus('[ScheduledJobsService] Stopping scheduled job polling');
105
+ this.engine.StopPolling();
106
+ this.isRunning = false;
107
+ LogStatus('[ScheduledJobsService] Polling stopped successfully');
108
+ } catch (error) {
109
+ LogError('[ScheduledJobsService] Error stopping polling', undefined, error);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get the system user for job execution
116
+ * Uses the email configured in scheduledJobs.systemUserEmail
117
+ */
118
+ private async getSystemUser(): Promise<UserInfo | null> {
119
+ const systemUserEmail = this.config.systemUserEmail;
120
+
121
+ // Search UserCache for system user
122
+ const user = UserCache.Users.find(u =>
123
+ u.Email?.toLowerCase() === systemUserEmail.toLowerCase()
124
+ );
125
+
126
+ if (user) {
127
+ return user;
128
+ }
129
+
130
+ LogError(`[ScheduledJobsService] System user not found with email: ${systemUserEmail}`);
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Get current service status
136
+ */
137
+ public GetStatus(): {
138
+ enabled: boolean;
139
+ running: boolean;
140
+ activeJobs: number;
141
+ pollingInterval: number;
142
+ } {
143
+ return {
144
+ enabled: this.config.enabled,
145
+ running: this.isRunning,
146
+ activeJobs: this.engine?.ScheduledJobs?.length || 0,
147
+ pollingInterval: this.engine?.ActivePollingInterval || 0
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Check if service is enabled in configuration
153
+ */
154
+ public get IsEnabled(): boolean {
155
+ return this.config.enabled;
156
+ }
157
+
158
+ /**
159
+ * Check if service is currently running
160
+ */
161
+ public get IsRunning(): boolean {
162
+ return this.isRunning;
163
+ }
164
+ }