UNPKG

@axinom/mosaic-transactional-inbox-outbox

Version:

This library encapsulates the Mosaic based transactional inbox and outbox pattern

197 lines (187 loc) 6.67 kB
import { MessageHandler, MessageInfo } from '@axinom/mosaic-message-bus'; import { MessagingSettings } from '@axinom/mosaic-message-bus-abstractions'; import { Mutex } from 'async-mutex'; import { Pool } from 'pg'; import { DEFAULT_INBOX_MESSAGE_TYPE, Dict, InboxOutboxLogger, UNKNOWN_AGGREGATE_ID, UNKNOWN_AGGREGATE_TYPE, } from '../common'; import { OptionalInboxData, StoreInboxMessage } from './setup-inbox-storage'; export interface InboxMessageInput extends OptionalInboxData { messageId: string; aggregateId: string; messagingSettings: Pick<MessagingSettings, 'messageType' | 'aggregateType'>; payload: unknown; } export interface CustomMessageMapper { (message: MessageInfo): InboxMessageInput | undefined; } export interface CustomMessagePreProcessor { (message: InboxMessageInput): void; } export interface InboxWriterCustomizations { /** * A custom message mapper for messages that do not follow the Mosaic messaging * pattern. Returns either an TransactionalMessage or undefined - in this case * the default mapping is used. */ customMessageMapper?: CustomMessageMapper; /** * A custom message processor to change message properties before storing the * inbox message. This can be useful to define the segment or the concurrency * processing. */ customMessagePreProcessor?: CustomMessagePreProcessor; /** * An optional list of all messages that are expected for backward compatibility. * Old clients may not send in the aggregate type yet so this will provide a * mapping to find the aggregate type by the event type only. */ acceptedMessageSettings?: MessagingSettings[]; } export class RabbitMqInboxWriter extends MessageHandler<unknown> { private customMessageMapper?: CustomMessageMapper; private customMessagePreProcessor?: CustomMessagePreProcessor; private backwardCompatibilityMappings: Dict<string>; private mutex = new Mutex(); /** * Creates a new RabbitMQ-based handler that receives all events and commands * and stores them in the transactional inbox. * @param storeInboxMessage Function to store the incoming RabbitMQ message in the transactional inbox * @param customizations Provide custom logic on how the inbox writer should process messages */ constructor( protected storeInboxMessage: StoreInboxMessage, protected ownerPool: Pool, protected logger: InboxOutboxLogger, customizations?: InboxWriterCustomizations, ) { super(DEFAULT_INBOX_MESSAGE_TYPE); this.customMessageMapper = customizations?.customMessageMapper; this.customMessagePreProcessor = customizations?.customMessagePreProcessor; this.backwardCompatibilityMappings = this.backwardCompatibilityMapper( customizations?.acceptedMessageSettings, ); } //TODO: [feature/image-transactional-in-out-box] remove backwards compatibility after 2024-02-01 backwardCompatibilityMapper = ( acceptedMessageSettings?: MessagingSettings[], ): Dict<string> => { const backwardCompatibilityMappings: Dict<string> = {}; for (const messageSettings of acceptedMessageSettings ?? []) { backwardCompatibilityMappings[messageSettings.messageType] = messageSettings.aggregateType; } return backwardCompatibilityMappings; }; /** * Store the incoming message in the transactional inbox. */ async onMessage( payload: unknown, message: MessageInfo<unknown>, ): Promise<void> { // Using a mutex to ensure that each message is completely inserted // in the original sort order into the inbox. const release = await this.mutex.acquire(); let msgInput: InboxMessageInput | undefined; try { if (this.customMessageMapper) { msgInput = this.customMessageMapper(message); } if (!msgInput) { const envelope = message.envelope; let aggregateType = envelope.aggregate_type; if (!aggregateType) { //TODO: [feature/image-transactional-in-out-box] remove backwards compatibility after 2024-02-01 aggregateType = this.backwardCompatibilityMappings[envelope.message_type] ?? UNKNOWN_AGGREGATE_TYPE; } let aggregateId = envelope.aggregate_id; if (!aggregateId) { //TODO: [feature/image-transactional-in-out-box] remove backwards compatibility after 2024-02-01 aggregateId = String( (payload as { id: string | number | undefined })?.id ?? UNKNOWN_AGGREGATE_ID, ); } msgInput = { messageId: envelope.message_id, // Primary key - ensures the message is stored only once aggregateId, messagingSettings: { aggregateType, messageType: envelope.message_type, }, payload, metadata: { envelopeTimestamp: envelope.timestamp, messageVersion: envelope.message_version, messageContext: envelope.message_context, authToken: envelope.auth_token, fields: message.fields, properties: message.properties, }, concurrency: 'parallel', segment: unsafeGetEnvironment(envelope.auth_token), }; } this.customMessagePreProcessor?.(msgInput); const { payload: p, aggregateId, messagingSettings, ...optionalData } = msgInput; // No transaction for single insert needed - directly using the pool await this.storeInboxMessage( aggregateId, messagingSettings, p, this.ownerPool, optionalData, ); this.logger.debug({ message: 'Message added to the inbox table.', details: { ...msgInput }, }); } catch (error) { const err = error instanceof Error ? error : new Error(String(error ?? 'unknown error')); this.logger.error(err, { message: 'Could not write the message to the inbox table.', details: { ...(msgInput ?? message), }, }); throw err; } finally { release(); } } } /** * Get the environment ID by parsing the JWT without checking the signature. * This is only used for "fair message processing" - the actual JWT check is * done in the message handler. */ const unsafeGetEnvironment = ( token: string | undefined, ): string | undefined => { if (!token) { return undefined; } try { const parsed = JSON.parse( Buffer.from(token.split('.')[1], 'base64').toString(), ); return parsed?.environmentId ?? undefined; } catch { return undefined; } };