diff --git a/node.js/messaging.md b/node.js/messaging.md index 74a18b65b..3656b2a80 100644 --- a/node.js/messaging.md +++ b/node.js/messaging.md @@ -14,11 +14,102 @@ status: released +## Overview + +In SAP Cloud Application Programming Model (CAP), messaging enables decoupled communication between services using events. +CAP distinguishes between the logical and technical messaging layers, separating business concerns from technical infrastructure and enabling both flexibility and scalability. + +The **logical layer** consists of three primary components: + +**Modeled Events**: Events are defined in CDS models with typed schemas, providing compile-time validation and IDE support. These events represent business occurrences like `'orderProcessed'`, or `'stockUpdated'`. + +**Event Topics**: Topics organize events into logical channels, allowing for structured event categorization and routing. Topics can be explicitly defined or derived from service and event names. + +**CAP Services**: Services act as event producers and consumers, using simple APIs like `srv.emit('reviewed', data)` and `srv.on('orderProcessed', handler)`. Services communicate using friendly event names without needing to know the underlying infrastructure details. + +The **technical layer** handles the actual message transport and delivery: + +**CAP Messaging Service**: Acts as the translation layer between logical events and technical infrastructure. It converts service-level event names to fully qualified names (e.g., `'OrderSrv.reviewed'`), manages topic resolution, message serialization, and routing logic. + +**Message Brokers**: Form the core of the technical infrastructure, handling message persistence, delivery guarantees, and cross-service communication. Examples include SAP Event Mesh, Apache Kafka, or Redis Streams. + +The message flow follows a clear path through both layers: + +**Outbound Flow (Publisher)**: A service calls `srv.emit('reviewed', data)` → CAP Messaging Service resolves the event name to a fully qualified topic (e.g., `OrderSrv.reviewed`) → Message is serialized and sent to the Event Broker → Broker stores and distributes the message to all subscribers. + +**Inbound Flow (Subscriber)**: Event Broker delivers message from subscribed topic → CAP Messaging Service receives the message → Service name and event name are resolved from the topic → Message is routed to the appropriate service handler via `srv.on('reviewed', handler)`. + +**Alternatively** custom handlers can bypass the service layer and work directly with the messaging service: + +```javascript +// Direct access to messaging service +const messaging = await cds.connect.to('messaging'); + +// Send messages directly with full topic control +await messaging.emit('custom.topic', data); + +// Receive messages directly from any topic +messaging.on('external.system.events', handler); +``` + +### Topic Resolution + +Topic resolution is an important part of the integration between logical and technical messaging layers. + +Every technical message requires a topic. If a CDS event is declared without an @topic annotation, CAP uses the event’s fully qualified name (FQN) as the topic (e.g., OrderSrv.reviewed). If an event is declared with an @topic: 'foo.bar' annotation, the specified string is used as the topic (e.g., foo.bar). + +### Emitting and Receiving Events + +#### Scenario 1: No @topic Annotation + +When a CDS event is declared without an @topic annotation in a service (for example, an event called 'reviewed' in the OrderSrv service), the messaging behavior follows this pattern: + +**Emitting Events:** +• Using the logical service API, you emit the event with just the event name ('reviewed') +• Using the technical messaging API, you emit using the fully qualified name ('OrderSrv.reviewed') + +**Broker Handling:** +• The message broker receives and routes the message using the topic 'OrderSrv.reviewed' + +**Receiving Events:** +• Using the logical service API, you listen for the event with just the event name ('reviewed') +• Using the technical messaging API, you listen using the fully qualified name ('OrderSrv.reviewed') + +#### Scenario 2: With @topic Annotation + +When a CDS event is declared with a custom @topic annotation (for example, an event called 'reviewed' with @topic: 'foo.bar' in the OrderSrv service), the messaging behavior follows this pattern: + +**Emitting Events:** +• Using the logical service API, you still emit the event with the event name ('reviewed') +• Using the technical messaging API, you emit using the custom topic name ('foo.bar') + +**Broker Handling:** +• The message broker receives and routes the message using the custom topic 'foo.bar' + +**Receiving Events:** +• Using the logical service API, you listen for the event with the event name ('reviewed') +• Using the technical messaging API, you listen using the custom topic name ('foo.bar') + +### Summary Table + + +| CDS Event Declaration | Emitting via `srv.emit` | Emitting via `messaging.emit` | Broker Topic | Receiving via `srv.on` | Receiving via `messaging.on` | +|------------------------------|-------------------------|-------------------------------|----------------------|------------------------|------------------------------| +| No `@topic` | `'reviewed'` | `'OrderSrv.reviewed'` | `OrderSrv.reviewed` | `'reviewed'` | `'OrderSrv.reviewed'` | +| With `@topic: 'foo.bar'` | `'reviewed'` | `'foo.bar'` | `foo.bar` | `'reviewed'` | `'foo.bar'` | + + +### Key Points + +- Logical service API (srv.emit, srv.on) uses the event name as declared in CDS. +- Technical messaging API (messaging.emit, messaging.on) uses the resolved topic name. +- If no @topic is specified, the topic defaults to the event’s fully qualified name. +- If @topic is specified, it overrides the default topic name. ## cds.**MessagingService** class - Class `cds.MessagingService` and subclasses thereof are technical services representing asynchronous messaging channels. - They can be used directly/low-level, or behind the scenes on higher-level service-to-service eventing. +Class `cds.MessagingService` and subclasses thereof are technical services representing asynchronous messaging channels. +They can be used directly/low-level, or behind the scenes on higher-level service-to-service eventing. ### class cds.**MessagingService** extends cds.Service @@ -55,7 +146,7 @@ In _srv/external/external.cds_: service ExternalService { event ExternalEvent { ID: UUID; - name: String; + rating: Decimal; } } ``` @@ -66,7 +157,7 @@ In _srv/own.cds_: service OwnService { event OwnEvent { ID: UUID; - name: String; + rating: Decimal; } } ``` @@ -94,7 +185,7 @@ Example: ```cds service OwnService { @topic: 'my.custom.topic' - event OwnEvent { ID: UUID; name: String; } + event OwnEvent { ID: UUID; rating: Decimal; } } ``` @@ -109,19 +200,19 @@ Example: const messaging = await cds.connect.to('messaging') this.after(['CREATE', 'UPDATE', 'DELETE'], 'Reviews', async (_, req) => { - const { subject } = req.data + const { ID } = req.data const { rating } = await cds.run( SELECT.one(['round(avg(rating),2) as rating']) .from(Reviews) - .where({ subject })) + .where({ ID })) // send to a topic - await messaging.emit('cap/msg/system/review/reviewed', - { subject, rating }) + await messaging.emit('cap/msg/system/my/custom/topic', + { ID, rating }) // alternative if you want to send custom headers - await messaging.emit({ event: 'cap/msg/system/review/reviewed', - data: { subject, rating }, + await messaging.emit({ event: 'cap/msg/system/my/custom/topic', + data: { ID, rating }, headers: { 'X-Correlation-ID': req.headers['X-Correlation-ID'] }}) }) ``` @@ -141,9 +232,9 @@ Example: const messaging = await cds.connect.to('messaging') // listen to a topic -messaging.on('cap/msg/system/review/reviewed', msg => { - const { subject, rating } = msg.data - return cds.run(UPDATE(Books, subject).with({ rating })) +messaging.on('cap/msg/system/my/custom/topic', msg => { + const { ID, rating } = msg.data + return cds.run(UPDATE(Books, ID).with({ rating })) }) ```