@morojs/moro 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +61 -7
- package/dist/core/config/file-loader.js +31 -25
- package/dist/core/config/file-loader.js.map +1 -1
- package/dist/core/config/schema.d.ts +2 -2
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/types.d.ts +147 -0
- package/dist/core/config/types.js +124 -0
- package/dist/core/config/types.js.map +1 -0
- package/dist/core/config/typescript-loader.d.ts +6 -0
- package/dist/core/config/typescript-loader.js +268 -0
- package/dist/core/config/typescript-loader.js.map +1 -0
- package/dist/core/config/validation.d.ts +18 -0
- package/dist/core/config/validation.js +134 -0
- package/dist/core/config/validation.js.map +1 -0
- package/dist/core/docs/openapi-generator.js +6 -6
- package/dist/core/docs/openapi-generator.js.map +1 -1
- package/dist/core/docs/schema-to-openapi.d.ts +7 -0
- package/dist/core/docs/schema-to-openapi.js +124 -0
- package/dist/core/docs/schema-to-openapi.js.map +1 -0
- package/dist/core/docs/zod-to-openapi.d.ts +2 -0
- package/dist/core/docs/zod-to-openapi.js.map +1 -1
- package/dist/core/events/event-bus.js +4 -0
- package/dist/core/events/event-bus.js.map +1 -1
- package/dist/core/framework.d.ts +29 -6
- package/dist/core/framework.js +117 -18
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.d.ts +33 -0
- package/dist/core/http/http-server.js +329 -28
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/networking/adapters/index.d.ts +3 -0
- package/dist/core/networking/adapters/index.js +10 -0
- package/dist/core/networking/adapters/index.js.map +1 -0
- package/dist/core/networking/adapters/socketio-adapter.d.ts +16 -0
- package/dist/core/networking/adapters/socketio-adapter.js +244 -0
- package/dist/core/networking/adapters/socketio-adapter.js.map +1 -0
- package/dist/core/networking/adapters/ws-adapter.d.ts +54 -0
- package/dist/core/networking/adapters/ws-adapter.js +383 -0
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -0
- package/dist/core/networking/websocket-adapter.d.ts +171 -0
- package/dist/core/networking/websocket-adapter.js +5 -0
- package/dist/core/networking/websocket-adapter.js.map +1 -0
- package/dist/core/networking/websocket-manager.d.ts +53 -17
- package/dist/core/networking/websocket-manager.js +166 -108
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/routing/index.d.ts +13 -13
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/utilities/container.d.ts +1 -0
- package/dist/core/utilities/container.js +11 -1
- package/dist/core/utilities/container.js.map +1 -1
- package/dist/core/validation/adapters.d.ts +51 -0
- package/dist/core/validation/adapters.js +135 -0
- package/dist/core/validation/adapters.js.map +1 -0
- package/dist/core/validation/index.d.ts +14 -11
- package/dist/core/validation/index.js +37 -26
- package/dist/core/validation/index.js.map +1 -1
- package/dist/core/validation/schema-interface.d.ts +36 -0
- package/dist/core/validation/schema-interface.js +68 -0
- package/dist/core/validation/schema-interface.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +8 -0
- package/dist/moro.js +339 -14
- package/dist/moro.js.map +1 -1
- package/dist/types/core.d.ts +17 -0
- package/package.json +42 -14
- package/src/core/config/file-loader.ts +34 -25
- package/src/core/config/schema.ts +1 -1
- package/src/core/config/types.ts +277 -0
- package/src/core/config/typescript-loader.ts +571 -0
- package/src/core/config/validation.ts +145 -0
- package/src/core/docs/openapi-generator.ts +7 -6
- package/src/core/docs/schema-to-openapi.ts +148 -0
- package/src/core/docs/zod-to-openapi.ts +2 -0
- package/src/core/events/event-bus.ts +5 -0
- package/src/core/framework.ts +121 -28
- package/src/core/http/http-server.ts +377 -28
- package/src/core/networking/adapters/index.ts +16 -0
- package/src/core/networking/adapters/socketio-adapter.ts +252 -0
- package/src/core/networking/adapters/ws-adapter.ts +425 -0
- package/src/core/networking/websocket-adapter.ts +217 -0
- package/src/core/networking/websocket-manager.ts +185 -127
- package/src/core/routing/index.ts +13 -13
- package/src/core/utilities/container.ts +14 -1
- package/src/core/validation/adapters.ts +147 -0
- package/src/core/validation/index.ts +60 -38
- package/src/core/validation/schema-interface.ts +100 -0
- package/src/index.ts +25 -2
- package/src/moro.ts +405 -15
- package/src/types/core.ts +18 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// TypeScript-based Configuration Validation
|
|
2
|
+
// Simple validation functions that replace Zod for config system
|
|
3
|
+
|
|
4
|
+
export class ConfigValidationError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
public field: string,
|
|
7
|
+
public value: unknown,
|
|
8
|
+
message: string
|
|
9
|
+
) {
|
|
10
|
+
super(`Configuration validation failed for '${field}': ${message}`);
|
|
11
|
+
this.name = 'ConfigValidationError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Type-safe validation functions
|
|
16
|
+
export function validatePort(value: unknown, field = 'port'): number {
|
|
17
|
+
const num = Number(value);
|
|
18
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
19
|
+
throw new ConfigValidationError(field, value, 'Must be a number between 1 and 65535');
|
|
20
|
+
}
|
|
21
|
+
return num;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateBoolean(value: unknown, field = 'boolean'): boolean {
|
|
25
|
+
if (value === 'true' || value === true) return true;
|
|
26
|
+
if (value === 'false' || value === false) return false;
|
|
27
|
+
if (value === '1' || value === 1) return true;
|
|
28
|
+
if (value === '0' || value === 0) return false;
|
|
29
|
+
throw new ConfigValidationError(field, value, 'Must be a boolean (true/false) or numeric (1/0)');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateNumber(
|
|
33
|
+
value: unknown,
|
|
34
|
+
field = 'number',
|
|
35
|
+
options: { min?: number; max?: number } = {}
|
|
36
|
+
): number {
|
|
37
|
+
const num = Number(value);
|
|
38
|
+
if (isNaN(num)) {
|
|
39
|
+
throw new ConfigValidationError(field, value, 'Must be a valid number');
|
|
40
|
+
}
|
|
41
|
+
if (options.min !== undefined && num < options.min) {
|
|
42
|
+
throw new ConfigValidationError(field, value, `Must be at least ${options.min}`);
|
|
43
|
+
}
|
|
44
|
+
if (options.max !== undefined && num > options.max) {
|
|
45
|
+
throw new ConfigValidationError(field, value, `Must be at most ${options.max}`);
|
|
46
|
+
}
|
|
47
|
+
return num;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateString(value: unknown, field = 'string'): string {
|
|
51
|
+
if (typeof value !== 'string') {
|
|
52
|
+
throw new ConfigValidationError(field, value, 'Must be a string');
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateUrl(value: unknown, field = 'url'): string {
|
|
58
|
+
const str = validateString(value, field);
|
|
59
|
+
try {
|
|
60
|
+
new URL(str);
|
|
61
|
+
return str;
|
|
62
|
+
} catch {
|
|
63
|
+
throw new ConfigValidationError(field, value, 'Must be a valid URL');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function validateEnum<T extends string>(
|
|
68
|
+
value: unknown,
|
|
69
|
+
validValues: readonly T[],
|
|
70
|
+
field = 'enum'
|
|
71
|
+
): T {
|
|
72
|
+
const str = validateString(value, field);
|
|
73
|
+
if (!validValues.includes(str as T)) {
|
|
74
|
+
throw new ConfigValidationError(field, value, `Must be one of: ${validValues.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
return str as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateArray(value: unknown, field = 'array'): unknown[] {
|
|
80
|
+
if (!Array.isArray(value)) {
|
|
81
|
+
// Try to parse comma-separated string
|
|
82
|
+
if (typeof value === 'string') {
|
|
83
|
+
return value
|
|
84
|
+
.split(',')
|
|
85
|
+
.map(s => s.trim())
|
|
86
|
+
.filter(s => s.length > 0);
|
|
87
|
+
}
|
|
88
|
+
throw new ConfigValidationError(field, value, 'Must be an array or comma-separated string');
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateStringArray(value: unknown, field = 'string array'): string[] {
|
|
94
|
+
const arr = validateArray(value, field);
|
|
95
|
+
return arr.map((item, index) => validateString(item, `${field}[${index}]`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateOptional<T>(
|
|
99
|
+
value: unknown,
|
|
100
|
+
validator: (value: unknown, field: string) => T,
|
|
101
|
+
field: string
|
|
102
|
+
): T | undefined {
|
|
103
|
+
if (value === undefined || value === null || value === '') {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return validator(value, field);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Coercion helpers for environment variables
|
|
110
|
+
export function coerceEnvValue(value: string): unknown {
|
|
111
|
+
// Handle common patterns in environment variables
|
|
112
|
+
|
|
113
|
+
// Null/undefined
|
|
114
|
+
if (value === '' || value === 'null' || value === 'undefined') {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Boolean
|
|
119
|
+
if (value === 'true' || value === 'false') {
|
|
120
|
+
return value === 'true';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Number (but not if it starts with 0 - could be port, zip code, etc.)
|
|
124
|
+
if (/^-?\d+(\.\d+)?$/.test(value) && !value.startsWith('0')) {
|
|
125
|
+
const num = Number(value);
|
|
126
|
+
if (!isNaN(num)) {
|
|
127
|
+
return num;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// JSON (for complex objects/arrays)
|
|
132
|
+
if (
|
|
133
|
+
(value.startsWith('{') && value.endsWith('}')) ||
|
|
134
|
+
(value.startsWith('[') && value.endsWith(']'))
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(value);
|
|
138
|
+
} catch {
|
|
139
|
+
// Not valid JSON, treat as string
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Return as string for all other cases
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Extracts route information from intelligent routing and generates OpenAPI 3.0 specs
|
|
3
3
|
|
|
4
4
|
import { CompiledRoute, RouteSchema } from '../routing';
|
|
5
|
-
import {
|
|
5
|
+
import { OpenAPISchema } from './zod-to-openapi';
|
|
6
|
+
import { schemaToOpenAPI, generateExampleFromValidationSchema } from './schema-to-openapi';
|
|
6
7
|
import { createFrameworkLogger } from '../logger';
|
|
7
8
|
|
|
8
9
|
const logger = createFrameworkLogger('OpenAPIGenerator');
|
|
@@ -241,7 +242,7 @@ export class OpenAPIGenerator {
|
|
|
241
242
|
|
|
242
243
|
// Path parameters
|
|
243
244
|
if (route.validation?.params) {
|
|
244
|
-
const paramSchema =
|
|
245
|
+
const paramSchema = schemaToOpenAPI(route.validation.params, this.options);
|
|
245
246
|
if (paramSchema.properties) {
|
|
246
247
|
for (const [name, schema] of Object.entries(paramSchema.properties)) {
|
|
247
248
|
parameters.push({
|
|
@@ -258,7 +259,7 @@ export class OpenAPIGenerator {
|
|
|
258
259
|
|
|
259
260
|
// Query parameters
|
|
260
261
|
if (route.validation?.query) {
|
|
261
|
-
const querySchema =
|
|
262
|
+
const querySchema = schemaToOpenAPI(route.validation.query, this.options);
|
|
262
263
|
if (querySchema.properties) {
|
|
263
264
|
for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
264
265
|
const isRequired = querySchema.required?.includes(name) || false;
|
|
@@ -276,7 +277,7 @@ export class OpenAPIGenerator {
|
|
|
276
277
|
|
|
277
278
|
// Header parameters
|
|
278
279
|
if (route.validation?.headers) {
|
|
279
|
-
const headerSchema =
|
|
280
|
+
const headerSchema = schemaToOpenAPI(route.validation.headers, this.options);
|
|
280
281
|
if (headerSchema.properties) {
|
|
281
282
|
for (const [name, schema] of Object.entries(headerSchema.properties)) {
|
|
282
283
|
const isRequired = headerSchema.required?.includes(name) || false;
|
|
@@ -309,9 +310,9 @@ export class OpenAPIGenerator {
|
|
|
309
310
|
};
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
const bodySchema =
|
|
313
|
+
const bodySchema = schemaToOpenAPI(route.validation.body, this.options);
|
|
313
314
|
const example = this.options.includeExamples
|
|
314
|
-
?
|
|
315
|
+
? generateExampleFromValidationSchema(route.validation.body)
|
|
315
316
|
: undefined;
|
|
316
317
|
|
|
317
318
|
return {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Universal Schema to OpenAPI Converter
|
|
2
|
+
// Converts ValidationSchema (Zod, Joi, etc.) to OpenAPI 3.0 schema definitions
|
|
3
|
+
|
|
4
|
+
import { ValidationSchema } from '../validation/schema-interface';
|
|
5
|
+
import { OpenAPISchema } from './zod-to-openapi';
|
|
6
|
+
import { createFrameworkLogger } from '../logger';
|
|
7
|
+
|
|
8
|
+
const logger = createFrameworkLogger('SchemaToOpenAPI');
|
|
9
|
+
|
|
10
|
+
// Check if a schema is a Zod schema
|
|
11
|
+
function isZodSchema(schema: any): boolean {
|
|
12
|
+
return (
|
|
13
|
+
schema && typeof schema === 'object' && schema._def && typeof schema.parseAsync === 'function'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check if schema is Joi
|
|
18
|
+
function isJoiSchema(schema: any): boolean {
|
|
19
|
+
return (
|
|
20
|
+
schema &&
|
|
21
|
+
typeof schema === 'object' &&
|
|
22
|
+
schema.type &&
|
|
23
|
+
typeof schema.validateAsync === 'function'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert any ValidationSchema to OpenAPI
|
|
28
|
+
export function schemaToOpenAPI(
|
|
29
|
+
schema: ValidationSchema,
|
|
30
|
+
options: { includeExamples?: boolean; includeDescriptions?: boolean } = {}
|
|
31
|
+
): OpenAPISchema {
|
|
32
|
+
// If it's a Zod schema, use the existing zod converter
|
|
33
|
+
if (isZodSchema(schema)) {
|
|
34
|
+
try {
|
|
35
|
+
// Import zod converter dynamically
|
|
36
|
+
const { zodToOpenAPI } = require('./zod-to-openapi');
|
|
37
|
+
return zodToOpenAPI(schema, options);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.warn('Zod converter not available, using fallback', String(error));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If it's a Joi schema, convert from Joi
|
|
44
|
+
if (isJoiSchema(schema)) {
|
|
45
|
+
return convertJoiToOpenAPI(schema as any, options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// For other schemas (custom validators, etc.), return a generic object schema
|
|
49
|
+
logger.debug('Using generic schema conversion for unknown validation type');
|
|
50
|
+
return {
|
|
51
|
+
type: 'object',
|
|
52
|
+
description: options.includeDescriptions ? 'Validated object' : undefined,
|
|
53
|
+
additionalProperties: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Generate example from any ValidationSchema
|
|
58
|
+
export function generateExampleFromValidationSchema(schema: ValidationSchema): any {
|
|
59
|
+
// If it's a Zod schema, use existing example generator
|
|
60
|
+
if (isZodSchema(schema)) {
|
|
61
|
+
try {
|
|
62
|
+
const { generateExampleFromSchema } = require('./zod-to-openapi');
|
|
63
|
+
return generateExampleFromSchema(schema);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.warn('Zod example generator not available', String(error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For other schemas, return a generic example
|
|
70
|
+
return {
|
|
71
|
+
example: 'Validated data structure',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Convert Joi schema to OpenAPI (basic implementation)
|
|
76
|
+
function convertJoiToOpenAPI(
|
|
77
|
+
joiSchema: any,
|
|
78
|
+
options: { includeDescriptions?: boolean }
|
|
79
|
+
): OpenAPISchema {
|
|
80
|
+
const schemaType = joiSchema.type;
|
|
81
|
+
|
|
82
|
+
switch (schemaType) {
|
|
83
|
+
case 'string':
|
|
84
|
+
return {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: options.includeDescriptions ? joiSchema._description : undefined,
|
|
87
|
+
minLength: joiSchema._rules?.find((r: any) => r.name === 'min')?.args?.limit,
|
|
88
|
+
maxLength: joiSchema._rules?.find((r: any) => r.name === 'max')?.args?.limit,
|
|
89
|
+
pattern: joiSchema._rules?.find((r: any) => r.name === 'pattern')?.args?.regex?.source,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'number':
|
|
93
|
+
return {
|
|
94
|
+
type: 'number',
|
|
95
|
+
description: options.includeDescriptions ? joiSchema._description : undefined,
|
|
96
|
+
minimum: joiSchema._rules?.find((r: any) => r.name === 'min')?.args?.limit,
|
|
97
|
+
maximum: joiSchema._rules?.find((r: any) => r.name === 'max')?.args?.limit,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
case 'boolean':
|
|
101
|
+
return {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
description: options.includeDescriptions ? joiSchema._description : undefined,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
case 'object': {
|
|
107
|
+
const properties: Record<string, OpenAPISchema> = {};
|
|
108
|
+
const required: string[] = [];
|
|
109
|
+
|
|
110
|
+
if (joiSchema._inner?.children) {
|
|
111
|
+
for (const child of joiSchema._inner.children) {
|
|
112
|
+
const key = child.key;
|
|
113
|
+
properties[key] = convertJoiToOpenAPI(child.schema, options);
|
|
114
|
+
|
|
115
|
+
if (child.schema._flags?.presence === 'required') {
|
|
116
|
+
required.push(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties,
|
|
124
|
+
required: required.length > 0 ? required : undefined,
|
|
125
|
+
description: options.includeDescriptions ? joiSchema._description : undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case 'array':
|
|
130
|
+
return {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: joiSchema._inner?.items?.[0]
|
|
133
|
+
? convertJoiToOpenAPI(joiSchema._inner.items[0], options)
|
|
134
|
+
: { type: 'object' },
|
|
135
|
+
description: options.includeDescriptions ? joiSchema._description : undefined,
|
|
136
|
+
minItems: joiSchema._rules?.find((r: any) => r.name === 'min')?.args?.limit,
|
|
137
|
+
maxItems: joiSchema._rules?.find((r: any) => r.name === 'max')?.args?.limit,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
logger.warn(`Unsupported Joi schema type: ${schemaType}`);
|
|
142
|
+
return {
|
|
143
|
+
type: 'object',
|
|
144
|
+
additionalProperties: true,
|
|
145
|
+
description: options.includeDescriptions ? 'Complex validation schema' : undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -33,6 +33,11 @@ export class MoroEventBus implements GlobalEventBus {
|
|
|
33
33
|
|
|
34
34
|
// Global event emission with full context and metrics
|
|
35
35
|
async emit<T = any>(event: string, data: T, context?: Partial<EventContext>): Promise<boolean> {
|
|
36
|
+
// Fast path: skip processing if no listeners
|
|
37
|
+
if (this.emitter.listenerCount(event) === 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
const startTime = Date.now();
|
|
37
42
|
|
|
38
43
|
const fullContext: EventContext = {
|
package/src/core/framework.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Core Moro Framework with Pluggable WebSocket Adapters
|
|
2
2
|
import { createServer, Server } from 'http';
|
|
3
3
|
import {
|
|
4
4
|
createSecureServer as createHttp2SecureServer,
|
|
5
5
|
createServer as createHttp2Server,
|
|
6
6
|
} from 'http2';
|
|
7
|
-
import { Server as SocketIOServer } from 'socket.io';
|
|
8
7
|
import { EventEmitter } from 'events';
|
|
9
8
|
import { MoroHttpServer, HttpRequest, HttpResponse, middleware } from './http';
|
|
10
9
|
import { Router } from './http';
|
|
@@ -16,6 +15,7 @@ import { MoroEventBus } from './events';
|
|
|
16
15
|
import { createFrameworkLogger, logger as globalLogger } from './logger';
|
|
17
16
|
import { ModuleConfig, InternalRouteDefinition } from '../types/module';
|
|
18
17
|
import { LogLevel, LoggerOptions } from '../types/logger';
|
|
18
|
+
import { WebSocketAdapter, WebSocketAdapterOptions } from './networking/websocket-adapter';
|
|
19
19
|
|
|
20
20
|
export interface MoroOptions {
|
|
21
21
|
http2?: boolean;
|
|
@@ -28,23 +28,27 @@ export interface MoroOptions {
|
|
|
28
28
|
enabled?: boolean;
|
|
29
29
|
threshold?: number;
|
|
30
30
|
};
|
|
31
|
-
websocket?:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
websocket?:
|
|
32
|
+
| {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
adapter?: WebSocketAdapter;
|
|
35
|
+
compression?: boolean;
|
|
36
|
+
customIdGenerator?: () => string;
|
|
37
|
+
options?: WebSocketAdapterOptions;
|
|
38
|
+
}
|
|
39
|
+
| false;
|
|
35
40
|
logger?: LoggerOptions | boolean;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
export class Moro extends EventEmitter {
|
|
39
44
|
private httpServer: MoroHttpServer;
|
|
40
45
|
private server: Server | any; // HTTP/2 server type
|
|
41
|
-
private
|
|
46
|
+
private websocketAdapter?: WebSocketAdapter;
|
|
42
47
|
private container: Container;
|
|
43
48
|
private moduleLoader: ModuleLoader;
|
|
44
|
-
private websocketManager
|
|
49
|
+
private websocketManager?: WebSocketManager;
|
|
45
50
|
private circuitBreakers = new Map<string, CircuitBreaker>();
|
|
46
51
|
private rateLimiters = new Map<string, Map<string, { count: number; resetTime: number }>>();
|
|
47
|
-
private ioInstance: SocketIOServer;
|
|
48
52
|
// Enterprise-grade event system
|
|
49
53
|
private eventBus: MoroEventBus;
|
|
50
54
|
// Framework logger
|
|
@@ -99,23 +103,12 @@ export class Moro extends EventEmitter {
|
|
|
99
103
|
this.server = this.httpServer.getServer();
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
this.io = new SocketIOServer(this.server, {
|
|
103
|
-
cors: { origin: '*' },
|
|
104
|
-
path: '/socket.io/',
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this.ioInstance = this.io;
|
|
108
106
|
this.container = new Container();
|
|
109
107
|
this.moduleLoader = new ModuleLoader(this.container);
|
|
110
|
-
this.websocketManager = new WebSocketManager(this.io, this.container);
|
|
111
|
-
|
|
112
|
-
// Configure WebSocket advanced features
|
|
113
|
-
if (options.websocket?.customIdGenerator) {
|
|
114
|
-
this.websocketManager.setCustomIdGenerator(options.websocket.customIdGenerator);
|
|
115
|
-
}
|
|
116
108
|
|
|
117
|
-
if
|
|
118
|
-
|
|
109
|
+
// Setup WebSocket adapter if enabled
|
|
110
|
+
if (options.websocket !== false) {
|
|
111
|
+
this.setupWebSockets(options.websocket || {});
|
|
119
112
|
}
|
|
120
113
|
|
|
121
114
|
// Initialize enterprise event bus
|
|
@@ -153,6 +146,71 @@ export class Moro extends EventEmitter {
|
|
|
153
146
|
this.httpServer.use(this.errorBoundaryMiddleware());
|
|
154
147
|
}
|
|
155
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Setup WebSocket adapter and manager
|
|
151
|
+
*/
|
|
152
|
+
private async setupWebSockets(wsConfig: any): Promise<void> {
|
|
153
|
+
try {
|
|
154
|
+
// Use provided adapter or try to auto-detect
|
|
155
|
+
if (wsConfig.adapter) {
|
|
156
|
+
this.websocketAdapter = wsConfig.adapter;
|
|
157
|
+
} else {
|
|
158
|
+
this.websocketAdapter = (await this.detectWebSocketAdapter()) || undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.websocketAdapter) {
|
|
162
|
+
await this.websocketAdapter.initialize(this.server, wsConfig.options);
|
|
163
|
+
this.websocketManager = new WebSocketManager(this.websocketAdapter, this.container);
|
|
164
|
+
|
|
165
|
+
// Configure adapter features
|
|
166
|
+
if (wsConfig.compression) {
|
|
167
|
+
this.websocketAdapter.setCompression(true);
|
|
168
|
+
}
|
|
169
|
+
if (wsConfig.customIdGenerator) {
|
|
170
|
+
this.websocketAdapter.setCustomIdGenerator(wsConfig.customIdGenerator);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.logger.info(
|
|
174
|
+
`WebSocket adapter initialized: ${this.websocketAdapter.getAdapterName()}`,
|
|
175
|
+
'WebSocketSetup'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.logger.warn(
|
|
180
|
+
'WebSocket setup failed, continuing without WebSocket support',
|
|
181
|
+
'WebSocketSetup',
|
|
182
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Auto-detect available WebSocket adapter
|
|
189
|
+
*/
|
|
190
|
+
private async detectWebSocketAdapter(): Promise<WebSocketAdapter | null> {
|
|
191
|
+
// Try socket.io first
|
|
192
|
+
try {
|
|
193
|
+
const { SocketIOAdapter } = await import('./networking/adapters');
|
|
194
|
+
return new SocketIOAdapter();
|
|
195
|
+
} catch {
|
|
196
|
+
// socket.io not available
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Try native ws library
|
|
200
|
+
try {
|
|
201
|
+
const { WSAdapter } = await import('./networking/adapters');
|
|
202
|
+
return new WSAdapter();
|
|
203
|
+
} catch {
|
|
204
|
+
// ws not available
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.logger.warn(
|
|
208
|
+
'No WebSocket adapter found. Install socket.io or ws for WebSocket support',
|
|
209
|
+
'AdapterDetection'
|
|
210
|
+
);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
156
214
|
private requestTrackingMiddleware() {
|
|
157
215
|
return (req: HttpRequest, res: HttpResponse, next: () => void) => {
|
|
158
216
|
const startTime = Date.now();
|
|
@@ -206,8 +264,31 @@ export class Moro extends EventEmitter {
|
|
|
206
264
|
}
|
|
207
265
|
|
|
208
266
|
// Public API for accessing Socket.IO server
|
|
267
|
+
/**
|
|
268
|
+
* Get WebSocket adapter (for backward compatibility)
|
|
269
|
+
* @deprecated Use getWebSocketAdapter() instead
|
|
270
|
+
*/
|
|
209
271
|
getIOServer() {
|
|
210
|
-
|
|
272
|
+
if (!this.websocketAdapter) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
'WebSocket adapter not available. Install socket.io or configure a WebSocket adapter.'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return this.websocketAdapter;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the WebSocket adapter
|
|
282
|
+
*/
|
|
283
|
+
getWebSocketAdapter(): WebSocketAdapter | undefined {
|
|
284
|
+
return this.websocketAdapter;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the WebSocket manager
|
|
289
|
+
*/
|
|
290
|
+
getWebSocketManager(): WebSocketManager | undefined {
|
|
291
|
+
return this.websocketManager;
|
|
211
292
|
}
|
|
212
293
|
|
|
213
294
|
async loadModule(moduleConfig: ModuleConfig): Promise<void> {
|
|
@@ -392,7 +473,7 @@ export class Moro extends EventEmitter {
|
|
|
392
473
|
: undefined,
|
|
393
474
|
events: moduleEventBus, // Use pre-created event bus
|
|
394
475
|
app: {
|
|
395
|
-
get: (key: string) => (key === 'io' ? this.
|
|
476
|
+
get: (key: string) => (key === 'io' ? this.websocketAdapter : undefined),
|
|
396
477
|
},
|
|
397
478
|
};
|
|
398
479
|
this.logger.debug(`Database available: ${!!requestToUse.database}`, 'Handler', {
|
|
@@ -466,7 +547,15 @@ export class Moro extends EventEmitter {
|
|
|
466
547
|
}
|
|
467
548
|
|
|
468
549
|
private async setupWebSocketHandlers(config: ModuleConfig): Promise<void> {
|
|
469
|
-
|
|
550
|
+
if (!this.websocketAdapter || !this.websocketManager) {
|
|
551
|
+
this.logger.warn(
|
|
552
|
+
`Module ${config.name} defines WebSocket handlers but no WebSocket adapter is available`,
|
|
553
|
+
'WebSocketSetup'
|
|
554
|
+
);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const namespace = this.websocketAdapter.createNamespace(`/${config.name}`);
|
|
470
559
|
|
|
471
560
|
for (const wsConfig of config.websockets || []) {
|
|
472
561
|
await this.websocketManager.registerHandler(namespace, wsConfig, config);
|
|
@@ -530,13 +619,17 @@ export class Moro extends EventEmitter {
|
|
|
530
619
|
// Compatibility method for existing controllers
|
|
531
620
|
set(key: string, value: any): void {
|
|
532
621
|
if (key === 'io') {
|
|
533
|
-
|
|
622
|
+
// Deprecated: Use websocket adapter instead
|
|
623
|
+
this.logger.warn(
|
|
624
|
+
'Setting io instance is deprecated. Use websocket adapter configuration.',
|
|
625
|
+
'Deprecated'
|
|
626
|
+
);
|
|
534
627
|
}
|
|
535
628
|
}
|
|
536
629
|
|
|
537
630
|
get(key: string): any {
|
|
538
631
|
if (key === 'io') {
|
|
539
|
-
return this.
|
|
632
|
+
return this.websocketAdapter;
|
|
540
633
|
}
|
|
541
634
|
return undefined;
|
|
542
635
|
}
|