@message-in-the-middle/core
Version:
Framework-agnostic middleware pattern for message queue processing. Core package with all middlewares.
250 lines • 9.91 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageMiddlewareManager = void 0;
const events_1 = require("events");
const pipeline_1 = require("./pipeline");
const DEFAULT_MAX_MESSAGE_SIZE = 256 * 1024;
class MessageMiddlewareManager extends events_1.EventEmitter {
inboundPipeline;
outboundPipeline;
resources = [];
maxMessageSize;
eventsEnabled;
emitMiddlewareEvents;
slowProcessingThreshold;
startedAt = new Date();
inboundProcessedCount = 0;
outboundProcessedCount = 0;
inboundErrorCount = 0;
outboundErrorCount = 0;
totalInboundProcessingTime = 0;
totalOutboundProcessingTime = 0;
constructor(options = {}) {
super();
this.inboundPipeline = new pipeline_1.MiddlewarePipeline();
this.outboundPipeline = new pipeline_1.MiddlewarePipeline();
this.maxMessageSize = options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE;
this.eventsEnabled = options.events?.enabled ?? true;
this.emitMiddlewareEvents = options.events?.emitMiddlewareEvents ?? false;
this.slowProcessingThreshold = options.events?.slowProcessingThreshold ?? 5000;
}
emit(event, ...args) {
if (!this.eventsEnabled) {
return false;
}
return super.emit(event, ...args);
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
use(middleware) {
if ('process' in middleware && typeof middleware.process === 'function') {
return this.addInboundMiddleware(middleware);
}
else if ('processOutbound' in middleware && typeof middleware.processOutbound === 'function') {
return this.addOutboundMiddleware(middleware);
}
else {
throw new Error('Invalid middleware: must implement either InboundMiddleware (process method) or OutboundMiddleware (processOutbound method)');
}
}
addInboundMiddleware(middleware) {
this.inboundPipeline.use(middleware);
if ('destroy' in middleware && typeof middleware.destroy === 'function') {
this.resources.push(middleware);
}
return this;
}
addOutboundMiddleware(middleware) {
this.outboundPipeline.use(middleware);
if ('destroy' in middleware && typeof middleware.destroy === 'function') {
this.resources.push(middleware);
}
return this;
}
async processInbound(message, raw, attributes) {
if (message === null || message === undefined) {
throw new Error('Message cannot be null or undefined. ' +
'Provide a valid message object to process.');
}
try {
const messageStr = JSON.stringify(message);
if (messageStr.length > this.maxMessageSize) {
throw new Error(`Message size (${messageStr.length} bytes) exceeds maximum allowed size (${this.maxMessageSize} bytes). ` +
`Consider increasing maxMessageSize in ManagerOptions or reducing message payload.`);
}
}
catch (error) {
if (error instanceof TypeError && error.message.includes('circular')) {
throw new Error('Message contains circular references and cannot be serialized. ' +
'Remove circular references before processing.');
}
throw error;
}
const startTime = Date.now();
const context = {
message,
raw,
metadata: {},
attributes: attributes || {},
};
this.emit('message:start', context);
try {
await this.inboundPipeline.execute(context);
const duration = Date.now() - startTime;
this.inboundProcessedCount++;
this.totalInboundProcessingTime += duration;
this.emit('message:complete', context, duration);
if (duration > this.slowProcessingThreshold) {
this.emit('slow:processing', context, duration);
}
return context;
}
catch (error) {
const duration = Date.now() - startTime;
this.inboundErrorCount++;
this.totalInboundProcessingTime += duration;
this.emit('message:error', error, context);
throw error;
}
}
async processOutbound(message, metadata = {}, attributes = {}) {
if (message === null || message === undefined) {
throw new Error('Message cannot be null or undefined. ' +
'Provide a valid message object to process.');
}
try {
const messageStr = JSON.stringify(message);
if (messageStr.length > this.maxMessageSize) {
throw new Error(`Message size (${messageStr.length} bytes) exceeds maximum allowed size (${this.maxMessageSize} bytes). ` +
`Consider increasing maxMessageSize in ManagerOptions or reducing message payload.`);
}
}
catch (error) {
if (error instanceof TypeError && error.message.includes('circular')) {
throw new Error('Message contains circular references and cannot be serialized. ' +
'Remove circular references before processing.');
}
throw error;
}
const startTime = Date.now();
const context = {
message,
metadata,
attributes,
};
this.emit('message:start', context);
try {
await this.outboundPipeline.execute(context);
const duration = Date.now() - startTime;
this.outboundProcessedCount++;
this.totalOutboundProcessingTime += duration;
this.emit('message:complete', context, duration);
return context;
}
catch (error) {
const duration = Date.now() - startTime;
this.outboundErrorCount++;
this.totalOutboundProcessingTime += duration;
this.emit('message:error', error, context);
throw error;
}
}
getMiddlewares() {
return {
inbound: this.inboundPipeline.middlewares.map((m) => ({
name: m.name,
metadata: m.metadata,
})),
outbound: this.outboundPipeline.middlewares.map((m) => ({
name: m.name,
metadata: m.metadata,
})),
};
}
getStats() {
return {
inboundProcessed: this.inboundProcessedCount,
outboundProcessed: this.outboundProcessedCount,
inboundErrors: this.inboundErrorCount,
outboundErrors: this.outboundErrorCount,
avgInboundProcessingTimeMs: this.inboundProcessedCount > 0
? this.totalInboundProcessingTime / this.inboundProcessedCount
: 0,
avgOutboundProcessingTimeMs: this.outboundProcessedCount > 0
? this.totalOutboundProcessingTime / this.outboundProcessedCount
: 0,
startedAt: this.startedAt,
uptimeMs: Date.now() - this.startedAt.getTime(),
};
}
async getHealth() {
const componentHealthChecks = [];
const errors = [];
for (const resource of this.resources) {
if (this.isHealthable(resource)) {
try {
const health = await resource.getHealth();
componentHealthChecks.push(health);
}
catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
componentHealthChecks.push({
status: 'unhealthy',
message: `Health check failed: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
}
let overallStatus = 'healthy';
if (errors.length > 0 || componentHealthChecks.some(h => h.status === 'unhealthy')) {
overallStatus = 'unhealthy';
}
else if (componentHealthChecks.some(h => h.status === 'degraded')) {
overallStatus = 'degraded';
}
else if (componentHealthChecks.some(h => h.status === 'unknown')) {
overallStatus = 'unknown';
}
return {
status: overallStatus,
message: overallStatus === 'healthy'
? 'All components healthy'
: `${componentHealthChecks.filter(h => h.status !== 'healthy').length} components unhealthy or degraded`,
details: {
components: componentHealthChecks,
resourceCount: this.resources.length,
healthableCount: componentHealthChecks.length,
},
timestamp: new Date(),
};
}
isHealthable(resource) {
return resource && typeof resource.getHealth === 'function';
}
async destroy() {
const errors = [];
for (const resource of this.resources) {
if (resource.destroy && typeof resource.destroy === 'function') {
try {
await resource.destroy();
}
catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
}
this.resources = [];
if (errors.length > 0) {
throw new AggregateError(errors, `Failed to destroy ${errors.length} resource(s)`);
}
}
}
exports.MessageMiddlewareManager = MessageMiddlewareManager;
//# sourceMappingURL=manager.js.map