@morojs/moro 1.6.6 → 1.6.8
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/README.md +20 -4
- package/dist/core/auth/morojs-adapter.js +17 -14
- package/dist/core/auth/morojs-adapter.js.map +1 -1
- package/dist/core/config/config-sources.js +44 -0
- package/dist/core/config/config-sources.js.map +1 -1
- package/dist/core/database/adapters/drizzle.js +5 -5
- package/dist/core/database/adapters/drizzle.js.map +1 -1
- package/dist/core/database/adapters/mongodb.js +5 -1
- package/dist/core/database/adapters/mongodb.js.map +1 -1
- package/dist/core/database/adapters/mysql.js +5 -1
- package/dist/core/database/adapters/mysql.js.map +1 -1
- package/dist/core/database/adapters/postgresql.js +1 -1
- package/dist/core/database/adapters/postgresql.js.map +1 -1
- package/dist/core/database/adapters/redis.js +2 -2
- package/dist/core/database/adapters/redis.js.map +1 -1
- package/dist/core/database/adapters/sqlite.js +5 -1
- package/dist/core/database/adapters/sqlite.js.map +1 -1
- package/dist/core/docs/index.js.map +1 -1
- package/dist/core/docs/simple-docs.js +2 -1
- package/dist/core/docs/simple-docs.js.map +1 -1
- package/dist/core/docs/swagger-ui.js +1 -0
- package/dist/core/docs/swagger-ui.js.map +1 -1
- package/dist/core/docs/zod-to-openapi.js +4 -0
- package/dist/core/docs/zod-to-openapi.js.map +1 -1
- package/dist/core/events/event-bus.d.ts +1 -1
- package/dist/core/events/event-bus.js +1 -0
- package/dist/core/events/event-bus.js.map +1 -1
- package/dist/core/framework.d.ts +1 -1
- package/dist/core/framework.js +3 -1
- package/dist/core/framework.js.map +1 -1
- package/dist/core/graphql/adapter.d.ts +73 -0
- package/dist/core/graphql/adapter.js +2 -0
- package/dist/core/graphql/adapter.js.map +1 -0
- package/dist/core/graphql/adapters/graphql-js-adapter.d.ts +26 -0
- package/dist/core/graphql/adapters/graphql-js-adapter.js +229 -0
- package/dist/core/graphql/adapters/graphql-js-adapter.js.map +1 -0
- package/dist/core/graphql/core.d.ts +60 -0
- package/dist/core/graphql/core.js +165 -0
- package/dist/core/graphql/core.js.map +1 -0
- package/dist/core/graphql/index.d.ts +4 -0
- package/dist/core/graphql/index.js +4 -0
- package/dist/core/graphql/index.js.map +1 -0
- package/dist/core/graphql/loader.d.ts +9 -0
- package/dist/core/graphql/loader.js +32 -0
- package/dist/core/graphql/loader.js.map +1 -0
- package/dist/core/graphql/types.d.ts +211 -0
- package/dist/core/graphql/types.js +2 -0
- package/dist/core/graphql/types.js.map +1 -0
- package/dist/core/http/http-server.js +31 -9
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/utils/uws-worker-clustering.d.ts +28 -0
- package/dist/core/http/utils/uws-worker-clustering.js +313 -0
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -0
- package/dist/core/http/uws-http-server.d.ts +3 -1
- package/dist/core/http/uws-http-server.js +37 -8
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/jobs/cron-parser.d.ts +62 -0
- package/dist/core/jobs/cron-parser.js +239 -0
- package/dist/core/jobs/cron-parser.js.map +1 -0
- package/dist/core/jobs/index.d.ts +12 -0
- package/dist/core/jobs/index.js +9 -0
- package/dist/core/jobs/index.js.map +1 -0
- package/dist/core/jobs/job-executor.d.ts +134 -0
- package/dist/core/jobs/job-executor.js +413 -0
- package/dist/core/jobs/job-executor.js.map +1 -0
- package/dist/core/jobs/job-scheduler.d.ts +214 -0
- package/dist/core/jobs/job-scheduler.js +551 -0
- package/dist/core/jobs/job-scheduler.js.map +1 -0
- package/dist/core/jobs/job-state-manager.d.ts +158 -0
- package/dist/core/jobs/job-state-manager.js +444 -0
- package/dist/core/jobs/job-state-manager.js.map +1 -0
- package/dist/core/jobs/leader-election.d.ts +124 -0
- package/dist/core/jobs/leader-election.js +481 -0
- package/dist/core/jobs/leader-election.js.map +1 -0
- package/dist/core/jobs/types.d.ts +151 -0
- package/dist/core/jobs/types.js +4 -0
- package/dist/core/jobs/types.js.map +1 -0
- package/dist/core/jobs/utils.d.ts +95 -0
- package/dist/core/jobs/utils.js +258 -0
- package/dist/core/jobs/utils.js.map +1 -0
- package/dist/core/logger/filters.js +2 -0
- package/dist/core/logger/filters.js.map +1 -1
- package/dist/core/logger/logger.js +7 -3
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/logger/outputs.js +2 -0
- package/dist/core/logger/outputs.js.map +1 -1
- package/dist/core/middleware/built-in/auth/helpers.js +1 -1
- package/dist/core/middleware/built-in/auth/helpers.js.map +1 -1
- package/dist/core/middleware/built-in/auth/jwt-helpers.js +1 -1
- package/dist/core/middleware/built-in/auth/jwt-helpers.js.map +1 -1
- package/dist/core/middleware/built-in/auth/providers.js +1 -1
- package/dist/core/middleware/built-in/auth/providers.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/file.js +3 -3
- package/dist/core/middleware/built-in/cache/adapters/cache/file.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/memory.js +1 -0
- package/dist/core/middleware/built-in/cache/adapters/cache/memory.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.js +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/azure.d.ts +8 -0
- package/dist/core/middleware/built-in/cdn/adapters/cdn/azure.js +100 -7
- package/dist/core/middleware/built-in/cdn/adapters/cdn/azure.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudflare.d.ts +6 -0
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudflare.js +97 -13
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudflare.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudfront.js +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudfront.js.map +1 -1
- package/dist/core/middleware/built-in/cookie/hook.d.ts +1 -1
- package/dist/core/middleware/built-in/cookie/hook.js +2 -2
- package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
- package/dist/core/middleware/built-in/csrf/core.js +1 -0
- package/dist/core/middleware/built-in/csrf/core.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/core.d.ts +11 -0
- package/dist/core/middleware/built-in/graphql/core.js +24 -0
- package/dist/core/middleware/built-in/graphql/core.js.map +1 -0
- package/dist/core/middleware/built-in/graphql/helpers.d.ts +69 -0
- package/dist/core/middleware/built-in/graphql/helpers.js +187 -0
- package/dist/core/middleware/built-in/graphql/helpers.js.map +1 -0
- package/dist/core/middleware/built-in/graphql/hook.d.ts +7 -0
- package/dist/core/middleware/built-in/graphql/hook.js +78 -0
- package/dist/core/middleware/built-in/graphql/hook.js.map +1 -0
- package/dist/core/middleware/built-in/graphql/index.d.ts +5 -0
- package/dist/core/middleware/built-in/graphql/index.js +5 -0
- package/dist/core/middleware/built-in/graphql/index.js.map +1 -0
- package/dist/core/middleware/built-in/graphql/middleware.d.ts +7 -0
- package/dist/core/middleware/built-in/graphql/middleware.js +54 -0
- package/dist/core/middleware/built-in/graphql/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/graphql/subscriptions.d.ts +20 -0
- package/dist/core/middleware/built-in/graphql/subscriptions.js +37 -0
- package/dist/core/middleware/built-in/graphql/subscriptions.js.map +1 -0
- package/dist/core/middleware/built-in/index.d.ts +2 -1
- package/dist/core/middleware/built-in/index.js +3 -0
- package/dist/core/middleware/built-in/index.js.map +1 -1
- package/dist/core/middleware/built-in/validation/core.js +4 -2
- package/dist/core/middleware/built-in/validation/core.js.map +1 -1
- package/dist/core/middleware/index.js +1 -0
- package/dist/core/middleware/index.js.map +1 -1
- package/dist/core/modules/auto-discovery.js +5 -4
- package/dist/core/modules/auto-discovery.js.map +1 -1
- package/dist/core/modules/modules.js.map +1 -1
- package/dist/core/networking/adapters/socketio-adapter.js +1 -1
- package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
- package/dist/core/networking/adapters/uws-adapter.js +7 -2
- package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
- package/dist/core/networking/adapters/ws-adapter.js +5 -2
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
- package/dist/core/networking/websocket-manager.js +2 -0
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/pooling/object-pool-manager.js +3 -0
- package/dist/core/pooling/object-pool-manager.js.map +1 -1
- package/dist/core/routing/app-integration.d.ts +3 -3
- package/dist/core/routing/app-integration.js +1 -1
- package/dist/core/routing/app-integration.js.map +1 -1
- package/dist/core/routing/index.d.ts +1 -1
- package/dist/core/routing/index.js +1 -1
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/runtime/base-adapter.js +3 -3
- package/dist/core/runtime/base-adapter.js.map +1 -1
- package/dist/core/runtime/cloudflare-workers-adapter.js +1 -1
- package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -1
- package/dist/core/runtime/node-adapter.d.ts +1 -1
- package/dist/core/runtime/node-adapter.js +7 -4
- package/dist/core/runtime/node-adapter.js.map +1 -1
- package/dist/core/runtime/vercel-edge-adapter.js +1 -0
- package/dist/core/runtime/vercel-edge-adapter.js.map +1 -1
- package/dist/core/utilities/circuit-breaker.d.ts +9 -2
- package/dist/core/utilities/circuit-breaker.js +32 -3
- package/dist/core/utilities/circuit-breaker.js.map +1 -1
- package/dist/core/utilities/container.js +6 -0
- package/dist/core/utilities/container.js.map +1 -1
- package/dist/core/utilities/hooks.js +4 -0
- package/dist/core/utilities/hooks.js.map +1 -1
- package/dist/core/validation/index.js +6 -1
- package/dist/core/validation/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +154 -1
- package/dist/moro.js +588 -11
- package/dist/moro.js.map +1 -1
- package/dist/types/config.d.ts +28 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/events.d.ts +1 -1
- package/dist/types/events.js +1 -0
- package/dist/types/events.js.map +1 -1
- package/dist/types/module.d.ts +2 -2
- package/package.json +21 -1
package/dist/moro.js
CHANGED
|
@@ -17,6 +17,16 @@ import { normalizeValidationError } from './core/validation/schema-interface.js'
|
|
|
17
17
|
import { initializeConfig } from './core/config/index.js';
|
|
18
18
|
// Runtime System Integration
|
|
19
19
|
import { createRuntimeAdapter } from './core/runtime/index.js';
|
|
20
|
+
// uWebSockets Worker Thread Clustering
|
|
21
|
+
import { UWSWorkerClusterManager } from './core/http/utils/uws-worker-clustering.js';
|
|
22
|
+
import { isMainThread } from 'worker_threads';
|
|
23
|
+
// Job System Integration
|
|
24
|
+
import { JobScheduler, JobHealthChecker, everyInterval, cronSchedule, } from './core/jobs/index.js';
|
|
25
|
+
// Lazy imports for GraphQL to avoid crashes when not installed
|
|
26
|
+
let GraphQLCore;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
+
let GraphQLSubscriptionManager;
|
|
29
|
+
let setupGraphQLSubscriptions;
|
|
20
30
|
export class Moro extends EventEmitter {
|
|
21
31
|
coreFramework;
|
|
22
32
|
routes = [];
|
|
@@ -49,6 +59,14 @@ export class Moro extends EventEmitter {
|
|
|
49
59
|
middlewareManager;
|
|
50
60
|
// Queued WebSocket registrations (for async adapter detection)
|
|
51
61
|
queuedWebSocketRegistrations = [];
|
|
62
|
+
// Job scheduling system
|
|
63
|
+
jobScheduler;
|
|
64
|
+
jobHealthChecker;
|
|
65
|
+
jobsStarted = false;
|
|
66
|
+
// GraphQL system
|
|
67
|
+
graphqlCore; // Use any to avoid import dependency
|
|
68
|
+
graphqlSubscriptionManager;
|
|
69
|
+
graphqlInitPromise;
|
|
52
70
|
constructor(options = {}) {
|
|
53
71
|
super(); // Call EventEmitter constructor
|
|
54
72
|
// Track if user explicitly set logger/logging options
|
|
@@ -110,6 +128,33 @@ export class Moro extends EventEmitter {
|
|
|
110
128
|
}
|
|
111
129
|
// Access enterprise event bus from core framework
|
|
112
130
|
this.eventBus = this.coreFramework.eventBus;
|
|
131
|
+
// Initialize job scheduler if enabled in config
|
|
132
|
+
// Default to enabled (true) unless explicitly disabled
|
|
133
|
+
const jobsEnabled = this.config.jobs?.enabled !== false && options.jobs?.enabled !== false;
|
|
134
|
+
if (jobsEnabled) {
|
|
135
|
+
const leaderElectionOptions = this.config.jobs?.leaderElection?.enabled !== false
|
|
136
|
+
? {
|
|
137
|
+
strategy: this.config.jobs?.leaderElection?.strategy ?? 'file',
|
|
138
|
+
lockPath: this.config.jobs?.leaderElection?.lockPath,
|
|
139
|
+
lockTimeout: this.config.jobs?.leaderElection?.lockTimeout,
|
|
140
|
+
heartbeatInterval: this.config.jobs?.leaderElection?.heartbeatInterval,
|
|
141
|
+
}
|
|
142
|
+
: undefined;
|
|
143
|
+
const jobSchedulerOptions = {
|
|
144
|
+
maxConcurrentJobs: this.config.jobs?.maxConcurrentJobs,
|
|
145
|
+
enableLeaderElection: this.config.jobs?.leaderElection?.enabled !== false,
|
|
146
|
+
leaderElection: leaderElectionOptions,
|
|
147
|
+
executor: this.config.jobs?.executor,
|
|
148
|
+
stateManager: this.config.jobs?.stateManager,
|
|
149
|
+
gracefulShutdownTimeout: this.config.jobs?.gracefulShutdownTimeout,
|
|
150
|
+
};
|
|
151
|
+
this.jobScheduler = new JobScheduler(createFrameworkLogger('Jobs'), jobSchedulerOptions);
|
|
152
|
+
this.jobHealthChecker = new JobHealthChecker(this.jobScheduler, createFrameworkLogger('JobHealth'));
|
|
153
|
+
this.logger.info('Job scheduler initialized', 'Jobs', {
|
|
154
|
+
maxConcurrentJobs: jobSchedulerOptions.maxConcurrentJobs,
|
|
155
|
+
leaderElection: jobSchedulerOptions.enableLeaderElection,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
113
158
|
// Setup default middleware if enabled - use config defaults with options override
|
|
114
159
|
this.setupDefaultMiddleware({
|
|
115
160
|
...this.getDefaultOptionsFromConfig(),
|
|
@@ -412,15 +457,16 @@ export class Moro extends EventEmitter {
|
|
|
412
457
|
throw new Error('Port not specified and not found in configuration. Please provide a port number or configure it in moro.config.js/ts');
|
|
413
458
|
}
|
|
414
459
|
// Check if clustering is enabled for massive performance gains
|
|
415
|
-
// NOTE: uWebSockets.js does NOT support Node.js clustering - it's single-threaded only
|
|
416
460
|
const usingUWebSockets = this.config.server?.useUWebSockets || false;
|
|
417
461
|
if (this.config.performance?.clustering?.enabled) {
|
|
418
462
|
if (usingUWebSockets) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
463
|
+
// Use worker thread clustering for uWebSockets
|
|
464
|
+
this.logger.info('uWebSockets clustering enabled - using worker threads with acceptor pattern', 'Cluster');
|
|
465
|
+
this.startWithUWSClustering(port, host, callback);
|
|
466
|
+
return;
|
|
422
467
|
}
|
|
423
468
|
else {
|
|
469
|
+
// Use traditional Node.js cluster module for standard HTTP
|
|
424
470
|
this.startWithClustering(port, host, callback);
|
|
425
471
|
return;
|
|
426
472
|
}
|
|
@@ -432,7 +478,7 @@ export class Moro extends EventEmitter {
|
|
|
432
478
|
this.coreFramework.addMiddleware(docsMiddleware);
|
|
433
479
|
this.logger.debug('Documentation middleware added', 'Documentation');
|
|
434
480
|
}
|
|
435
|
-
catch
|
|
481
|
+
catch {
|
|
436
482
|
// Documentation not enabled, that's fine
|
|
437
483
|
this.logger.debug('Documentation not enabled', 'Documentation');
|
|
438
484
|
}
|
|
@@ -478,6 +524,10 @@ export class Moro extends EventEmitter {
|
|
|
478
524
|
this.unifiedRouter.logPerformanceStats();
|
|
479
525
|
}
|
|
480
526
|
this.eventBus.emit('server:started', { port, runtime: this.runtimeType });
|
|
527
|
+
// Start job scheduler after server starts
|
|
528
|
+
this.startJobScheduler().catch(err => {
|
|
529
|
+
this.logger.error(`Failed to start job scheduler: ${String(err)}`);
|
|
530
|
+
});
|
|
481
531
|
if (callback)
|
|
482
532
|
callback();
|
|
483
533
|
};
|
|
@@ -604,7 +654,7 @@ export class Moro extends EventEmitter {
|
|
|
604
654
|
if (res.headersSent)
|
|
605
655
|
return;
|
|
606
656
|
}
|
|
607
|
-
catch
|
|
657
|
+
catch {
|
|
608
658
|
// Documentation not enabled, that's fine
|
|
609
659
|
}
|
|
610
660
|
// Try unified router first (handles all routes)
|
|
@@ -622,6 +672,7 @@ export class Moro extends EventEmitter {
|
|
|
622
672
|
// Handle direct routes for runtime adapters
|
|
623
673
|
async handleDirectRoutes(req, res) {
|
|
624
674
|
// Find matching route
|
|
675
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
625
676
|
const route = this.findMatchingRoute(req.method, req.path);
|
|
626
677
|
if (!route) {
|
|
627
678
|
res.status(404).json({ success: false, error: 'Not found' });
|
|
@@ -790,6 +841,7 @@ export class Moro extends EventEmitter {
|
|
|
790
841
|
if (!this.dynamicRoutesBySegments.has(segmentCount)) {
|
|
791
842
|
this.dynamicRoutesBySegments.set(segmentCount, []);
|
|
792
843
|
}
|
|
844
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
793
845
|
this.dynamicRoutesBySegments.get(segmentCount).push(route);
|
|
794
846
|
}
|
|
795
847
|
}
|
|
@@ -1146,6 +1198,7 @@ export class Moro extends EventEmitter {
|
|
|
1146
1198
|
const gracefulShutdown = () => {
|
|
1147
1199
|
this.logger.info('Gracefully shutting down cluster...', 'Cluster');
|
|
1148
1200
|
// Clean up all workers
|
|
1201
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1149
1202
|
for (const [pid, worker] of this.clusterWorkers) {
|
|
1150
1203
|
worker.removeAllListeners();
|
|
1151
1204
|
worker.kill('SIGTERM');
|
|
@@ -1160,6 +1213,7 @@ export class Moro extends EventEmitter {
|
|
|
1160
1213
|
// Fork workers with basic tracking
|
|
1161
1214
|
for (let i = 0; i < workerCount; i++) {
|
|
1162
1215
|
const worker = cluster.fork();
|
|
1216
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1163
1217
|
this.clusterWorkers.set(worker.process.pid, worker);
|
|
1164
1218
|
this.logger.info(`Worker ${worker.process.pid} started`, 'Cluster');
|
|
1165
1219
|
// Handle individual worker messages
|
|
@@ -1173,6 +1227,7 @@ export class Moro extends EventEmitter {
|
|
|
1173
1227
|
this.logger.warn(`Worker ${pid} died unexpectedly (${signal || code}). Restarting...`, 'Cluster');
|
|
1174
1228
|
// Simple restart
|
|
1175
1229
|
const newWorker = cluster.fork();
|
|
1230
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1176
1231
|
this.clusterWorkers.set(newWorker.process.pid, newWorker);
|
|
1177
1232
|
this.logger.info(`Worker ${newWorker.process.pid} restarted`, 'Cluster');
|
|
1178
1233
|
}
|
|
@@ -1254,7 +1309,7 @@ export class Moro extends EventEmitter {
|
|
|
1254
1309
|
const docsMiddleware = this.documentation.getDocsMiddleware();
|
|
1255
1310
|
this.coreFramework.addMiddleware(docsMiddleware);
|
|
1256
1311
|
}
|
|
1257
|
-
catch
|
|
1312
|
+
catch {
|
|
1258
1313
|
// Documentation not enabled, that's fine
|
|
1259
1314
|
}
|
|
1260
1315
|
// Add unified routing middleware (handles both chainable and direct routes)
|
|
@@ -1329,18 +1384,209 @@ export class Moro extends EventEmitter {
|
|
|
1329
1384
|
// Log other worker messages
|
|
1330
1385
|
this.logger.debug(`Worker message: ${JSON.stringify(message)}`, 'Cluster');
|
|
1331
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* uWebSockets Worker Thread Clustering Implementation
|
|
1389
|
+
* Uses worker threads with acceptor pattern for maximum performance
|
|
1390
|
+
*/
|
|
1391
|
+
async startWithUWSClustering(port, host, callback) {
|
|
1392
|
+
// Check if we're in a worker thread spawned by UWSWorkerClusterManager
|
|
1393
|
+
if (UWSWorkerClusterManager.isUWSWorker()) {
|
|
1394
|
+
// Worker thread mode - setup the app and send descriptor to acceptor
|
|
1395
|
+
await this.startUWSWorker(port, host, callback);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
// Main thread mode - create acceptor and spawn workers
|
|
1399
|
+
if (isMainThread) {
|
|
1400
|
+
await this.startUWSAcceptor(port, host, callback);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async startUWSAcceptor(port, host, callback) {
|
|
1405
|
+
const clusterManager = new UWSWorkerClusterManager({
|
|
1406
|
+
workers: this.config.performance?.clustering?.workers,
|
|
1407
|
+
memoryPerWorkerGB: this.config.performance?.clustering?.memoryPerWorkerGB,
|
|
1408
|
+
port,
|
|
1409
|
+
host,
|
|
1410
|
+
ssl: this.config.server?.ssl,
|
|
1411
|
+
});
|
|
1412
|
+
try {
|
|
1413
|
+
await clusterManager.startAcceptorAndWorkers(() => {
|
|
1414
|
+
// This factory function is not used in our implementation
|
|
1415
|
+
// as we're using process.argv[1] to spawn workers
|
|
1416
|
+
return null;
|
|
1417
|
+
});
|
|
1418
|
+
if (callback) {
|
|
1419
|
+
callback();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
catch (error) {
|
|
1423
|
+
this.logger.error('Failed to start uWebSockets cluster', 'UWSCluster', {
|
|
1424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1425
|
+
});
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async startUWSWorker(port, host, callback) {
|
|
1430
|
+
this.logger.info(`UWS Worker thread ${process.pid} initializing`, 'UWSWorker');
|
|
1431
|
+
// Reduce logging contention in workers
|
|
1432
|
+
if (!this.userSetLogger) {
|
|
1433
|
+
applyLoggingConfiguration(undefined, { level: 'warn' });
|
|
1434
|
+
}
|
|
1435
|
+
// Worker-specific optimizations
|
|
1436
|
+
process.env.UV_THREADPOOL_SIZE = '64';
|
|
1437
|
+
// Setup graceful shutdown for worker
|
|
1438
|
+
let isShuttingDown = false;
|
|
1439
|
+
const shutdownWorker = async () => {
|
|
1440
|
+
if (isShuttingDown)
|
|
1441
|
+
return;
|
|
1442
|
+
isShuttingDown = true;
|
|
1443
|
+
this.logger.info(`UWS Worker thread ${process.pid} shutting down...`, 'UWSWorker');
|
|
1444
|
+
// Close the server to clean up handles
|
|
1445
|
+
const httpServer = this.coreFramework.httpServer;
|
|
1446
|
+
if (httpServer && typeof httpServer.close === 'function') {
|
|
1447
|
+
// Convert callback-based close to Promise
|
|
1448
|
+
await new Promise(resolve => {
|
|
1449
|
+
httpServer.close(() => {
|
|
1450
|
+
resolve();
|
|
1451
|
+
});
|
|
1452
|
+
// Timeout after 1.5 seconds
|
|
1453
|
+
setTimeout(() => {
|
|
1454
|
+
this.logger.warn('HTTP server close timeout', 'UWSWorker');
|
|
1455
|
+
resolve();
|
|
1456
|
+
}, 1500);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
// Clean up ALL event listeners
|
|
1460
|
+
try {
|
|
1461
|
+
this.eventBus.removeAllListeners();
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
// Ignore cleanup errors
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
this.removeAllListeners();
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
// Ignore cleanup errors
|
|
1471
|
+
}
|
|
1472
|
+
// Remove all signal handlers
|
|
1473
|
+
process.removeAllListeners('SIGINT');
|
|
1474
|
+
process.removeAllListeners('SIGTERM');
|
|
1475
|
+
this.logger.info(`UWS Worker thread ${process.pid} cleanup complete`, 'UWSWorker');
|
|
1476
|
+
};
|
|
1477
|
+
// Handle shutdown messages from parent
|
|
1478
|
+
UWSWorkerClusterManager.setupWorkerShutdownHandler(shutdownWorker);
|
|
1479
|
+
// Also handle direct signals
|
|
1480
|
+
process.once('SIGINT', shutdownWorker);
|
|
1481
|
+
process.once('SIGTERM', shutdownWorker);
|
|
1482
|
+
// Emit worker starting event
|
|
1483
|
+
this.eventBus.emit('server:starting', {
|
|
1484
|
+
port,
|
|
1485
|
+
runtime: this.runtimeType,
|
|
1486
|
+
worker: process.pid,
|
|
1487
|
+
workerType: 'thread',
|
|
1488
|
+
});
|
|
1489
|
+
// Add documentation middleware first (if enabled)
|
|
1490
|
+
try {
|
|
1491
|
+
const docsMiddleware = this.documentation.getDocsMiddleware();
|
|
1492
|
+
this.coreFramework.addMiddleware(docsMiddleware);
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
// Documentation not enabled, that's fine
|
|
1496
|
+
}
|
|
1497
|
+
// Add unified routing middleware
|
|
1498
|
+
this.coreFramework.addMiddleware(async (req, res, next) => {
|
|
1499
|
+
const handled = this.unifiedRouter.handleRequest(req, res);
|
|
1500
|
+
if (handled && typeof handled.then === 'function') {
|
|
1501
|
+
const isHandled = await handled;
|
|
1502
|
+
if (!isHandled) {
|
|
1503
|
+
next();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
if (!handled) {
|
|
1508
|
+
next();
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
// Register legacy direct routes with the HTTP server
|
|
1513
|
+
if (this.routes.length > 0) {
|
|
1514
|
+
this.registerDirectRoutes();
|
|
1515
|
+
}
|
|
1516
|
+
const workerCallback = () => {
|
|
1517
|
+
const displayHost = host || 'localhost';
|
|
1518
|
+
this.logger.info(`UWS Worker thread ${process.pid} ready on ${displayHost}:${port}`, 'UWSWorker');
|
|
1519
|
+
this.eventBus.emit('server:started', {
|
|
1520
|
+
port,
|
|
1521
|
+
runtime: this.runtimeType,
|
|
1522
|
+
worker: process.pid,
|
|
1523
|
+
workerType: 'thread',
|
|
1524
|
+
});
|
|
1525
|
+
// Send descriptor to acceptor
|
|
1526
|
+
const httpServer = this.coreFramework.httpServer;
|
|
1527
|
+
if (httpServer && typeof httpServer.getDescriptor === 'function') {
|
|
1528
|
+
UWSWorkerClusterManager.sendDescriptorToAcceptor(httpServer.getApp())
|
|
1529
|
+
.then(() => {
|
|
1530
|
+
if (callback) {
|
|
1531
|
+
callback();
|
|
1532
|
+
}
|
|
1533
|
+
})
|
|
1534
|
+
.catch((error) => {
|
|
1535
|
+
this.logger.error('Failed to send descriptor to acceptor', 'UWSWorker', {
|
|
1536
|
+
error: error.message,
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
this.logger.error('HTTP server does not support getDescriptor()', 'UWSWorker');
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
// Ensure WebSocket setup is complete before starting worker
|
|
1545
|
+
await this.processQueuedWebSocketRegistrations();
|
|
1546
|
+
// Start listening on worker-specific port (4000 range)
|
|
1547
|
+
const workerPort = 4000 + parseInt(process.env.UWS_WORKER_INDEX || '0', 10);
|
|
1548
|
+
if (host) {
|
|
1549
|
+
this.coreFramework.listen(workerPort, host, workerCallback);
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
this.coreFramework.listen(workerPort, workerCallback);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1332
1555
|
/**
|
|
1333
1556
|
* Gracefully close the application and clean up resources
|
|
1334
1557
|
* This should be called in tests and during shutdown
|
|
1335
1558
|
*/
|
|
1336
1559
|
async close() {
|
|
1337
1560
|
this.logger.debug('Closing Moro application...');
|
|
1561
|
+
// Shutdown job scheduler first
|
|
1562
|
+
if (this.jobScheduler) {
|
|
1563
|
+
try {
|
|
1564
|
+
this.logger.info('Shutting down job scheduler...');
|
|
1565
|
+
await this.jobScheduler.shutdown();
|
|
1566
|
+
this.jobsStarted = false;
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
this.logger.error(`Error shutting down job scheduler: ${String(err)}`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
// Cleanup GraphQL executor timers
|
|
1573
|
+
if (this.graphqlCore) {
|
|
1574
|
+
try {
|
|
1575
|
+
const executor = this.graphqlCore.getExecutor();
|
|
1576
|
+
if (executor) {
|
|
1577
|
+
executor.cleanup();
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
catch (err) {
|
|
1581
|
+
this.logger.error(`Error cleaning up GraphQL: ${String(err)}`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1338
1584
|
// Flush logger buffer before shutdown
|
|
1339
1585
|
try {
|
|
1340
1586
|
// Use flushBuffer for immediate synchronous flush
|
|
1341
1587
|
this.logger.flushBuffer();
|
|
1342
1588
|
}
|
|
1343
|
-
catch
|
|
1589
|
+
catch {
|
|
1344
1590
|
// Ignore flush errors during shutdown
|
|
1345
1591
|
}
|
|
1346
1592
|
// Close the core framework with timeout
|
|
@@ -1355,7 +1601,7 @@ export class Moro extends EventEmitter {
|
|
|
1355
1601
|
new Promise(resolve => setTimeout(resolve, 2000)), // 2 second timeout
|
|
1356
1602
|
]);
|
|
1357
1603
|
}
|
|
1358
|
-
catch
|
|
1604
|
+
catch {
|
|
1359
1605
|
// Force close if graceful close fails
|
|
1360
1606
|
this.logger.warn('Force closing HTTP server due to timeout');
|
|
1361
1607
|
}
|
|
@@ -1365,7 +1611,7 @@ export class Moro extends EventEmitter {
|
|
|
1365
1611
|
try {
|
|
1366
1612
|
this.moduleDiscovery.cleanup();
|
|
1367
1613
|
}
|
|
1368
|
-
catch
|
|
1614
|
+
catch {
|
|
1369
1615
|
// Ignore cleanup errors
|
|
1370
1616
|
}
|
|
1371
1617
|
}
|
|
@@ -1374,11 +1620,342 @@ export class Moro extends EventEmitter {
|
|
|
1374
1620
|
this.eventBus.removeAllListeners();
|
|
1375
1621
|
this.removeAllListeners();
|
|
1376
1622
|
}
|
|
1377
|
-
catch
|
|
1623
|
+
catch {
|
|
1378
1624
|
// Ignore cleanup errors
|
|
1379
1625
|
}
|
|
1380
1626
|
this.logger.debug('Moro application closed successfully');
|
|
1381
1627
|
}
|
|
1628
|
+
// ========================================
|
|
1629
|
+
// Job Scheduling API
|
|
1630
|
+
// ========================================
|
|
1631
|
+
/**
|
|
1632
|
+
* Register a background job with cron or interval schedule
|
|
1633
|
+
* @param name - Job name (used for identification)
|
|
1634
|
+
* @param schedule - Cron expression, interval string ('5m', '1h'), or schedule object
|
|
1635
|
+
* @param handler - Job function to execute
|
|
1636
|
+
* @param options - Job configuration options
|
|
1637
|
+
* @returns Job ID for management
|
|
1638
|
+
*
|
|
1639
|
+
* @example
|
|
1640
|
+
* // Cron schedule
|
|
1641
|
+
* app.job('cleanup', '0 2 * * *', async () => {
|
|
1642
|
+
* await cleanupOldData();
|
|
1643
|
+
* });
|
|
1644
|
+
*
|
|
1645
|
+
* // Interval schedule
|
|
1646
|
+
* app.job('health-check', '5m', async (ctx) => {
|
|
1647
|
+
* console.log('Health check', ctx.executionId);
|
|
1648
|
+
* });
|
|
1649
|
+
*
|
|
1650
|
+
* // Advanced options
|
|
1651
|
+
* app.job('report', '@daily', generateReport, {
|
|
1652
|
+
* timeout: 60000,
|
|
1653
|
+
* maxRetries: 3,
|
|
1654
|
+
* onError: (ctx, error) => console.error('Job failed', error)
|
|
1655
|
+
* });
|
|
1656
|
+
*/
|
|
1657
|
+
job(name, schedule, handler, options = {}) {
|
|
1658
|
+
if (!this.jobScheduler) {
|
|
1659
|
+
throw new Error('Job scheduler is not enabled. Set config.jobs.enabled = true');
|
|
1660
|
+
}
|
|
1661
|
+
let jobSchedule;
|
|
1662
|
+
// Parse schedule input
|
|
1663
|
+
if (typeof schedule === 'string') {
|
|
1664
|
+
// Check if it's a cron expression or interval
|
|
1665
|
+
if (schedule.match(/^(\d+[smhd]|\d+\s*(seconds?|minutes?|hours?|days?))$/i)) {
|
|
1666
|
+
// Interval format
|
|
1667
|
+
jobSchedule = everyInterval(schedule);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
// Cron format
|
|
1671
|
+
jobSchedule = cronSchedule(schedule, options.timezone);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
jobSchedule = schedule;
|
|
1676
|
+
}
|
|
1677
|
+
const jobOptions = {
|
|
1678
|
+
name: options.name || name,
|
|
1679
|
+
enabled: options.enabled,
|
|
1680
|
+
priority: options.priority,
|
|
1681
|
+
timezone: options.timezone,
|
|
1682
|
+
maxConcurrent: options.maxConcurrent,
|
|
1683
|
+
timeout: options.timeout,
|
|
1684
|
+
maxRetries: options.maxRetries,
|
|
1685
|
+
retryDelay: options.retryDelay,
|
|
1686
|
+
retryBackoff: options.retryBackoff,
|
|
1687
|
+
enableCircuitBreaker: options.enableCircuitBreaker,
|
|
1688
|
+
metadata: options.metadata,
|
|
1689
|
+
onStart: options.onStart,
|
|
1690
|
+
onComplete: options.onComplete,
|
|
1691
|
+
onError: options.onError,
|
|
1692
|
+
};
|
|
1693
|
+
const jobId = this.jobScheduler.registerJob(name, jobSchedule, handler, jobOptions);
|
|
1694
|
+
this.logger.info(`Job registered: ${name} (${jobId})`);
|
|
1695
|
+
return jobId;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Enable or disable a registered job
|
|
1699
|
+
*/
|
|
1700
|
+
setJobEnabled(jobId, enabled) {
|
|
1701
|
+
if (!this.jobScheduler) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
return this.jobScheduler.setJobEnabled(jobId, enabled);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Manually trigger a job execution
|
|
1708
|
+
*/
|
|
1709
|
+
async triggerJob(jobId, metadata) {
|
|
1710
|
+
if (!this.jobScheduler) {
|
|
1711
|
+
throw new Error('Job scheduler is not enabled');
|
|
1712
|
+
}
|
|
1713
|
+
return this.jobScheduler.triggerJob(jobId, metadata);
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Unregister a job
|
|
1717
|
+
*/
|
|
1718
|
+
unregisterJob(jobId) {
|
|
1719
|
+
if (!this.jobScheduler) {
|
|
1720
|
+
throw new Error('Job scheduler is not enabled');
|
|
1721
|
+
}
|
|
1722
|
+
return this.jobScheduler.unregisterJob(jobId);
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Get job metrics
|
|
1726
|
+
*/
|
|
1727
|
+
getJobMetrics(jobId) {
|
|
1728
|
+
if (!this.jobScheduler) {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
return this.jobScheduler.getJobMetrics(jobId);
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Get job health status
|
|
1735
|
+
*/
|
|
1736
|
+
getJobHealth(jobId) {
|
|
1737
|
+
if (!this.jobHealthChecker) {
|
|
1738
|
+
if (jobId) {
|
|
1739
|
+
return {
|
|
1740
|
+
jobId,
|
|
1741
|
+
name: 'Unknown',
|
|
1742
|
+
status: 'unknown',
|
|
1743
|
+
enabled: false,
|
|
1744
|
+
consecutiveFailures: 0,
|
|
1745
|
+
message: 'Job scheduler not enabled',
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
return [];
|
|
1749
|
+
}
|
|
1750
|
+
if (jobId) {
|
|
1751
|
+
return this.jobHealthChecker.checkJobHealth(jobId);
|
|
1752
|
+
}
|
|
1753
|
+
return this.jobHealthChecker.checkAllJobs();
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Get scheduler statistics
|
|
1757
|
+
*/
|
|
1758
|
+
getJobStats() {
|
|
1759
|
+
if (!this.jobScheduler) {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
return this.jobScheduler.getStats();
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Get overall scheduler health
|
|
1766
|
+
*/
|
|
1767
|
+
getSchedulerHealth() {
|
|
1768
|
+
if (!this.jobHealthChecker) {
|
|
1769
|
+
return {
|
|
1770
|
+
status: 'unknown',
|
|
1771
|
+
message: 'Job scheduler not enabled',
|
|
1772
|
+
stats: null,
|
|
1773
|
+
jobs: [],
|
|
1774
|
+
unhealthyJobCount: 0,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
return this.jobHealthChecker.getSchedulerHealth();
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Start the job scheduler (called automatically on listen)
|
|
1781
|
+
*/
|
|
1782
|
+
async startJobScheduler() {
|
|
1783
|
+
if (!this.jobScheduler || this.jobsStarted) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
await this.jobScheduler.start();
|
|
1788
|
+
this.jobsStarted = true;
|
|
1789
|
+
this.logger.info('Job scheduler started');
|
|
1790
|
+
}
|
|
1791
|
+
catch (err) {
|
|
1792
|
+
this.logger.error(`Failed to start job scheduler: ${String(err)}`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// ========================================
|
|
1796
|
+
// GraphQL API
|
|
1797
|
+
// ========================================
|
|
1798
|
+
/**
|
|
1799
|
+
* Configure GraphQL endpoint with schema, resolvers, and options
|
|
1800
|
+
*
|
|
1801
|
+
* @param options - GraphQL configuration options
|
|
1802
|
+
*
|
|
1803
|
+
* @example
|
|
1804
|
+
* ```ts
|
|
1805
|
+
* // Using type definitions and resolvers
|
|
1806
|
+
* app.graphql({
|
|
1807
|
+
* typeDefs: `
|
|
1808
|
+
* type Query {
|
|
1809
|
+
* hello: String
|
|
1810
|
+
* users: [User]
|
|
1811
|
+
* }
|
|
1812
|
+
* type User {
|
|
1813
|
+
* id: ID!
|
|
1814
|
+
* name: String!
|
|
1815
|
+
* }
|
|
1816
|
+
* `,
|
|
1817
|
+
* resolvers: {
|
|
1818
|
+
* Query: {
|
|
1819
|
+
* hello: () => 'Hello World!',
|
|
1820
|
+
* users: () => [{ id: '1', name: 'Alice' }]
|
|
1821
|
+
* }
|
|
1822
|
+
* }
|
|
1823
|
+
* });
|
|
1824
|
+
*
|
|
1825
|
+
* // Using Pothos schema builder
|
|
1826
|
+
* import SchemaBuilder from '@pothos/core';
|
|
1827
|
+
*
|
|
1828
|
+
* const builder = new SchemaBuilder();
|
|
1829
|
+
* builder.queryType({
|
|
1830
|
+
* fields: (t) => ({
|
|
1831
|
+
* hello: t.string({ resolve: () => 'Hello World!' })
|
|
1832
|
+
* })
|
|
1833
|
+
* });
|
|
1834
|
+
*
|
|
1835
|
+
* app.graphql({
|
|
1836
|
+
* pothosSchema: builder
|
|
1837
|
+
* });
|
|
1838
|
+
* ```
|
|
1839
|
+
*/
|
|
1840
|
+
graphql(options) {
|
|
1841
|
+
if (this.graphqlCore || this.graphqlInitPromise) {
|
|
1842
|
+
throw new Error('GraphQL has already been configured. Call graphql() only once.');
|
|
1843
|
+
}
|
|
1844
|
+
// Check if graphql package is available
|
|
1845
|
+
if (!this.isGraphQLAvailable()) {
|
|
1846
|
+
throw new Error('GraphQL support requires the graphql package to be installed.\n' +
|
|
1847
|
+
'Install it with: npm install graphql\n' +
|
|
1848
|
+
'For TypeScript-first GraphQL, also consider: npm install @pothos/core\n' +
|
|
1849
|
+
'For performance boost: npm install graphql-jit');
|
|
1850
|
+
}
|
|
1851
|
+
this.logger.info('Configuring GraphQL', 'GraphQL', {
|
|
1852
|
+
path: options.path || '/graphql',
|
|
1853
|
+
jit: options.enableJIT !== false,
|
|
1854
|
+
playground: options.enablePlayground !== false,
|
|
1855
|
+
});
|
|
1856
|
+
// Initialize GraphQL asynchronously (like WebSocket registration pattern)
|
|
1857
|
+
this.graphqlInitPromise = this.initializeGraphQL(options);
|
|
1858
|
+
// Add to initialization promises
|
|
1859
|
+
const originalEnsure = this.ensureAutoDiscoveryComplete.bind(this);
|
|
1860
|
+
this.ensureAutoDiscoveryComplete = async () => {
|
|
1861
|
+
await originalEnsure();
|
|
1862
|
+
await this.graphqlInitPromise;
|
|
1863
|
+
};
|
|
1864
|
+
return this;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Initialize GraphQL system asynchronously
|
|
1868
|
+
*/
|
|
1869
|
+
async initializeGraphQL(options) {
|
|
1870
|
+
try {
|
|
1871
|
+
// Lazy load GraphQL modules
|
|
1872
|
+
await this.loadGraphQLModules();
|
|
1873
|
+
// Create GraphQL core
|
|
1874
|
+
this.graphqlCore = new GraphQLCore(options);
|
|
1875
|
+
// Initialize GraphQL
|
|
1876
|
+
await this.graphqlCore.initialize();
|
|
1877
|
+
this.logger.info('GraphQL initialized successfully', 'GraphQL');
|
|
1878
|
+
// Setup subscriptions if enabled
|
|
1879
|
+
if (options.enableSubscriptions !== false && this.config.websocket.enabled) {
|
|
1880
|
+
this.setupGraphQLSubscriptions(options);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
catch (error) {
|
|
1884
|
+
this.logger.error('Failed to initialize GraphQL', 'GraphQL', { error });
|
|
1885
|
+
throw error;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Check if GraphQL package is available
|
|
1890
|
+
*/
|
|
1891
|
+
isGraphQLAvailable() {
|
|
1892
|
+
try {
|
|
1893
|
+
require.resolve('graphql');
|
|
1894
|
+
return true;
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Lazy load GraphQL modules
|
|
1902
|
+
*/
|
|
1903
|
+
async loadGraphQLModules() {
|
|
1904
|
+
if (GraphQLCore) {
|
|
1905
|
+
return; // Already loaded
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
// GraphQL core now uses adapters with no static imports from 'graphql'
|
|
1909
|
+
const coreModule = await import('./core/graphql/core.js');
|
|
1910
|
+
GraphQLCore = coreModule.GraphQLCore;
|
|
1911
|
+
const subsModule = await import('./core/middleware/built-in/graphql/subscriptions.js');
|
|
1912
|
+
GraphQLSubscriptionManager = subsModule.GraphQLSubscriptionManager;
|
|
1913
|
+
setupGraphQLSubscriptions = subsModule.setupGraphQLSubscriptions;
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
this.logger.error('Failed to load GraphQL modules', 'GraphQL', { error });
|
|
1917
|
+
throw new Error('Failed to load GraphQL modules. Please ensure graphql is installed.');
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Setup GraphQL subscriptions
|
|
1922
|
+
*/
|
|
1923
|
+
setupGraphQLSubscriptions(options) {
|
|
1924
|
+
const websocketAdapter = this.coreFramework.getWebSocketAdapter();
|
|
1925
|
+
if (!websocketAdapter) {
|
|
1926
|
+
this.logger.warn('GraphQL subscriptions require WebSocket to be enabled', 'GraphQL');
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (!this.graphqlCore) {
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const schema = this.graphqlCore.getSchema();
|
|
1933
|
+
this.graphqlSubscriptionManager = setupGraphQLSubscriptions(websocketAdapter, schema, {
|
|
1934
|
+
path: options.path ? `${options.path}/subscriptions` : '/graphql/subscriptions',
|
|
1935
|
+
contextFactory: options.context,
|
|
1936
|
+
});
|
|
1937
|
+
this.logger.info('GraphQL subscriptions enabled', 'GraphQL', {
|
|
1938
|
+
path: options.path ? `${options.path}/subscriptions` : '/graphql/subscriptions',
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get GraphQL schema (if configured)
|
|
1943
|
+
*/
|
|
1944
|
+
getGraphQLSchema() {
|
|
1945
|
+
return this.graphqlCore?.getSchema();
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Get GraphQL stats
|
|
1949
|
+
*/
|
|
1950
|
+
getGraphQLStats() {
|
|
1951
|
+
if (!this.graphqlCore) {
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
...this.graphqlCore.getStats(),
|
|
1956
|
+
subscriptions: this.graphqlSubscriptionManager?.getSubscriptionCount() || 0,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1382
1959
|
}
|
|
1383
1960
|
// Export convenience function
|
|
1384
1961
|
export function createApp(options) {
|