@morojs/moro 1.6.5 → 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 +8 -4
- 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.d.ts +7 -0
- package/dist/core/http/http-server.js +267 -123
- 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 +7 -1
- package/dist/core/http/uws-http-server.js +272 -189
- 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.d.ts +7 -5
- package/dist/core/logger/logger.js +68 -27
- 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/rate-limit/core.d.ts +5 -0
- package/dist/core/middleware/built-in/rate-limit/core.js +16 -8
- package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
- package/dist/core/middleware/built-in/validation/core.js +42 -19
- 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.d.ts +8 -2
- package/dist/core/pooling/object-pool-manager.js +38 -18
- 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/routing/path-matcher.d.ts +6 -0
- package/dist/core/routing/path-matcher.js +46 -7
- package/dist/core/routing/path-matcher.js.map +1 -1
- package/dist/core/routing/unified-router.d.ts +4 -0
- package/dist/core/routing/unified-router.js +104 -43
- package/dist/core/routing/unified-router.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.d.ts +4 -0
- package/dist/core/utilities/hooks.js +134 -22
- 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 +592 -16
- 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/logger.d.ts +1 -0
- package/dist/types/module.d.ts +2 -2
- package/package.json +21 -1
package/dist/moro.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createFrameworkLogger, applyLoggingConfiguration } from './core/logger/
|
|
|
7
7
|
import { MiddlewareManager } from './core/middleware/index.js';
|
|
8
8
|
import { IntelligentRoutingManager } from './core/routing/app-integration.js';
|
|
9
9
|
import { UnifiedRouter, } from './core/routing/unified-router.js';
|
|
10
|
+
import { PathMatcher } from './core/routing/path-matcher.js';
|
|
10
11
|
import { AppDocumentationManager } from './core/docs/index.js';
|
|
11
12
|
import { EventEmitter } from 'events';
|
|
12
13
|
import cluster from 'cluster';
|
|
@@ -16,6 +17,16 @@ import { normalizeValidationError } from './core/validation/schema-interface.js'
|
|
|
16
17
|
import { initializeConfig } from './core/config/index.js';
|
|
17
18
|
// Runtime System Integration
|
|
18
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;
|
|
19
30
|
export class Moro extends EventEmitter {
|
|
20
31
|
coreFramework;
|
|
21
32
|
routes = [];
|
|
@@ -48,6 +59,14 @@ export class Moro extends EventEmitter {
|
|
|
48
59
|
middlewareManager;
|
|
49
60
|
// Queued WebSocket registrations (for async adapter detection)
|
|
50
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;
|
|
51
70
|
constructor(options = {}) {
|
|
52
71
|
super(); // Call EventEmitter constructor
|
|
53
72
|
// Track if user explicitly set logger/logging options
|
|
@@ -109,6 +128,33 @@ export class Moro extends EventEmitter {
|
|
|
109
128
|
}
|
|
110
129
|
// Access enterprise event bus from core framework
|
|
111
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
|
+
}
|
|
112
158
|
// Setup default middleware if enabled - use config defaults with options override
|
|
113
159
|
this.setupDefaultMiddleware({
|
|
114
160
|
...this.getDefaultOptionsFromConfig(),
|
|
@@ -411,15 +457,16 @@ export class Moro extends EventEmitter {
|
|
|
411
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');
|
|
412
458
|
}
|
|
413
459
|
// Check if clustering is enabled for massive performance gains
|
|
414
|
-
// NOTE: uWebSockets.js does NOT support Node.js clustering - it's single-threaded only
|
|
415
460
|
const usingUWebSockets = this.config.server?.useUWebSockets || false;
|
|
416
461
|
if (this.config.performance?.clustering?.enabled) {
|
|
417
462
|
if (usingUWebSockets) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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;
|
|
421
467
|
}
|
|
422
468
|
else {
|
|
469
|
+
// Use traditional Node.js cluster module for standard HTTP
|
|
423
470
|
this.startWithClustering(port, host, callback);
|
|
424
471
|
return;
|
|
425
472
|
}
|
|
@@ -431,7 +478,7 @@ export class Moro extends EventEmitter {
|
|
|
431
478
|
this.coreFramework.addMiddleware(docsMiddleware);
|
|
432
479
|
this.logger.debug('Documentation middleware added', 'Documentation');
|
|
433
480
|
}
|
|
434
|
-
catch
|
|
481
|
+
catch {
|
|
435
482
|
// Documentation not enabled, that's fine
|
|
436
483
|
this.logger.debug('Documentation not enabled', 'Documentation');
|
|
437
484
|
}
|
|
@@ -477,6 +524,10 @@ export class Moro extends EventEmitter {
|
|
|
477
524
|
this.unifiedRouter.logPerformanceStats();
|
|
478
525
|
}
|
|
479
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
|
+
});
|
|
480
531
|
if (callback)
|
|
481
532
|
callback();
|
|
482
533
|
};
|
|
@@ -603,7 +654,7 @@ export class Moro extends EventEmitter {
|
|
|
603
654
|
if (res.headersSent)
|
|
604
655
|
return;
|
|
605
656
|
}
|
|
606
|
-
catch
|
|
657
|
+
catch {
|
|
607
658
|
// Documentation not enabled, that's fine
|
|
608
659
|
}
|
|
609
660
|
// Try unified router first (handles all routes)
|
|
@@ -621,6 +672,7 @@ export class Moro extends EventEmitter {
|
|
|
621
672
|
// Handle direct routes for runtime adapters
|
|
622
673
|
async handleDirectRoutes(req, res) {
|
|
623
674
|
// Find matching route
|
|
675
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
624
676
|
const route = this.findMatchingRoute(req.method, req.path);
|
|
625
677
|
if (!route) {
|
|
626
678
|
res.status(404).json({ success: false, error: 'Not found' });
|
|
@@ -707,8 +759,7 @@ export class Moro extends EventEmitter {
|
|
|
707
759
|
};
|
|
708
760
|
}
|
|
709
761
|
// Phase 2: Optimized dynamic route matching by segment count
|
|
710
|
-
const
|
|
711
|
-
const segmentCount = segments.length;
|
|
762
|
+
const segmentCount = PathMatcher.countSegments(path);
|
|
712
763
|
const candidateRoutes = this.dynamicRoutesBySegments.get(segmentCount) || [];
|
|
713
764
|
for (const route of candidateRoutes) {
|
|
714
765
|
if (route.method === method) {
|
|
@@ -786,11 +837,11 @@ export class Moro extends EventEmitter {
|
|
|
786
837
|
}
|
|
787
838
|
else {
|
|
788
839
|
// Dynamic route - organize by segment count
|
|
789
|
-
const
|
|
790
|
-
const segmentCount = segments.length;
|
|
840
|
+
const segmentCount = PathMatcher.countSegments(route.path);
|
|
791
841
|
if (!this.dynamicRoutesBySegments.has(segmentCount)) {
|
|
792
842
|
this.dynamicRoutesBySegments.set(segmentCount, []);
|
|
793
843
|
}
|
|
844
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
794
845
|
this.dynamicRoutesBySegments.get(segmentCount).push(route);
|
|
795
846
|
}
|
|
796
847
|
}
|
|
@@ -1147,6 +1198,7 @@ export class Moro extends EventEmitter {
|
|
|
1147
1198
|
const gracefulShutdown = () => {
|
|
1148
1199
|
this.logger.info('Gracefully shutting down cluster...', 'Cluster');
|
|
1149
1200
|
// Clean up all workers
|
|
1201
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1150
1202
|
for (const [pid, worker] of this.clusterWorkers) {
|
|
1151
1203
|
worker.removeAllListeners();
|
|
1152
1204
|
worker.kill('SIGTERM');
|
|
@@ -1161,6 +1213,7 @@ export class Moro extends EventEmitter {
|
|
|
1161
1213
|
// Fork workers with basic tracking
|
|
1162
1214
|
for (let i = 0; i < workerCount; i++) {
|
|
1163
1215
|
const worker = cluster.fork();
|
|
1216
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1164
1217
|
this.clusterWorkers.set(worker.process.pid, worker);
|
|
1165
1218
|
this.logger.info(`Worker ${worker.process.pid} started`, 'Cluster');
|
|
1166
1219
|
// Handle individual worker messages
|
|
@@ -1174,6 +1227,7 @@ export class Moro extends EventEmitter {
|
|
|
1174
1227
|
this.logger.warn(`Worker ${pid} died unexpectedly (${signal || code}). Restarting...`, 'Cluster');
|
|
1175
1228
|
// Simple restart
|
|
1176
1229
|
const newWorker = cluster.fork();
|
|
1230
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1177
1231
|
this.clusterWorkers.set(newWorker.process.pid, newWorker);
|
|
1178
1232
|
this.logger.info(`Worker ${newWorker.process.pid} restarted`, 'Cluster');
|
|
1179
1233
|
}
|
|
@@ -1212,7 +1266,7 @@ export class Moro extends EventEmitter {
|
|
|
1212
1266
|
'--turbo-fast-api-calls', // Optimize API calls
|
|
1213
1267
|
'--turbo-escape-analysis', // Escape analysis optimization
|
|
1214
1268
|
'--turbo-inline-api-calls', // Inline API calls
|
|
1215
|
-
|
|
1269
|
+
`--max-old-space-size=${heapSizePerWorkerMB}`, // Limit memory to prevent GC pressure
|
|
1216
1270
|
];
|
|
1217
1271
|
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ' ' + v8Flags.join(' ');
|
|
1218
1272
|
}
|
|
@@ -1255,7 +1309,7 @@ export class Moro extends EventEmitter {
|
|
|
1255
1309
|
const docsMiddleware = this.documentation.getDocsMiddleware();
|
|
1256
1310
|
this.coreFramework.addMiddleware(docsMiddleware);
|
|
1257
1311
|
}
|
|
1258
|
-
catch
|
|
1312
|
+
catch {
|
|
1259
1313
|
// Documentation not enabled, that's fine
|
|
1260
1314
|
}
|
|
1261
1315
|
// Add unified routing middleware (handles both chainable and direct routes)
|
|
@@ -1330,18 +1384,209 @@ export class Moro extends EventEmitter {
|
|
|
1330
1384
|
// Log other worker messages
|
|
1331
1385
|
this.logger.debug(`Worker message: ${JSON.stringify(message)}`, 'Cluster');
|
|
1332
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
|
+
}
|
|
1333
1555
|
/**
|
|
1334
1556
|
* Gracefully close the application and clean up resources
|
|
1335
1557
|
* This should be called in tests and during shutdown
|
|
1336
1558
|
*/
|
|
1337
1559
|
async close() {
|
|
1338
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
|
+
}
|
|
1339
1584
|
// Flush logger buffer before shutdown
|
|
1340
1585
|
try {
|
|
1341
1586
|
// Use flushBuffer for immediate synchronous flush
|
|
1342
1587
|
this.logger.flushBuffer();
|
|
1343
1588
|
}
|
|
1344
|
-
catch
|
|
1589
|
+
catch {
|
|
1345
1590
|
// Ignore flush errors during shutdown
|
|
1346
1591
|
}
|
|
1347
1592
|
// Close the core framework with timeout
|
|
@@ -1356,7 +1601,7 @@ export class Moro extends EventEmitter {
|
|
|
1356
1601
|
new Promise(resolve => setTimeout(resolve, 2000)), // 2 second timeout
|
|
1357
1602
|
]);
|
|
1358
1603
|
}
|
|
1359
|
-
catch
|
|
1604
|
+
catch {
|
|
1360
1605
|
// Force close if graceful close fails
|
|
1361
1606
|
this.logger.warn('Force closing HTTP server due to timeout');
|
|
1362
1607
|
}
|
|
@@ -1366,7 +1611,7 @@ export class Moro extends EventEmitter {
|
|
|
1366
1611
|
try {
|
|
1367
1612
|
this.moduleDiscovery.cleanup();
|
|
1368
1613
|
}
|
|
1369
|
-
catch
|
|
1614
|
+
catch {
|
|
1370
1615
|
// Ignore cleanup errors
|
|
1371
1616
|
}
|
|
1372
1617
|
}
|
|
@@ -1375,11 +1620,342 @@ export class Moro extends EventEmitter {
|
|
|
1375
1620
|
this.eventBus.removeAllListeners();
|
|
1376
1621
|
this.removeAllListeners();
|
|
1377
1622
|
}
|
|
1378
|
-
catch
|
|
1623
|
+
catch {
|
|
1379
1624
|
// Ignore cleanup errors
|
|
1380
1625
|
}
|
|
1381
1626
|
this.logger.debug('Moro application closed successfully');
|
|
1382
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
|
+
}
|
|
1383
1959
|
}
|
|
1384
1960
|
// Export convenience function
|
|
1385
1961
|
export function createApp(options) {
|