eva4j 1.0.16 → 1.0.18
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/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# Command `generate rabbitmq-listener` (alias: `g rabbitmq-listener`)
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Creates individual RabbitMQ listener classes in a module's infrastructure layer. Each listener class is dedicated to a single queue, following the **Open/Closed Principle** for better maintainability and scalability.
|
|
6
|
+
|
|
7
|
+
The command generates Spring AMQP `@RabbitListener` components that automatically consume events from configured queues, deserialize them into `EventEnvelope` objects, and provide integration with the `UseCaseMediator` for processing. Messages are manually acknowledged and failed messages are routed to a dead-letter queue (DLQ).
|
|
8
|
+
|
|
9
|
+
**Key Feature:** Each queue gets its own listener class (e.g., `UserUserCreatedListener.java`, `OrderOrderPlacedListener.java`), with module-prefixed names to avoid bean conflicts when multiple modules subscribe to the same event.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
|
|
15
|
+
- **Create RabbitMQ consumers** to receive events from external systems
|
|
16
|
+
- **Process async events** using Spring AMQP listeners
|
|
17
|
+
- **Follow Open/Closed Principle** with individual listener classes per queue
|
|
18
|
+
- **Integrate with CQRS** architecture via UseCaseMediator
|
|
19
|
+
- **Handle errors with DLQ** — failed messages are nack'd to the dead-letter queue
|
|
20
|
+
- **Support multiple queues** by generating multiple listener classes
|
|
21
|
+
- **Enable event-driven communication** between microservices
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Syntax
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
eva generate rabbitmq-listener <module>
|
|
29
|
+
eva g rabbitmq-listener <module> # Short alias
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Parameters
|
|
33
|
+
|
|
34
|
+
| Parameter | Required | Description |
|
|
35
|
+
|-----------|----------|-------------|
|
|
36
|
+
| `module` | Yes | Name of the module where listener will be created |
|
|
37
|
+
|
|
38
|
+
### Interactive Prompts
|
|
39
|
+
|
|
40
|
+
After running the command, you'll be prompted to:
|
|
41
|
+
|
|
42
|
+
1. **Select queues to listen to** (multiple selection with space bar)
|
|
43
|
+
- Queues are read from `rabbitmq.yaml` configuration
|
|
44
|
+
- Must select at least one queue
|
|
45
|
+
- Can select multiple queues at once
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Prerequisites
|
|
50
|
+
|
|
51
|
+
### 1. RabbitMQ Client Must Be Installed
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
eva add rabbitmq-client
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This configures:
|
|
58
|
+
- Spring AMQP dependencies
|
|
59
|
+
- RabbitMQ consumer configuration with manual acknowledgment
|
|
60
|
+
- Queue management in `rabbitmq.yaml`
|
|
61
|
+
- EventEnvelope infrastructure
|
|
62
|
+
- RabbitMQConfig.java with retry + DLQ support
|
|
63
|
+
|
|
64
|
+
### 2. Queues Must Exist in rabbitmq.yaml
|
|
65
|
+
|
|
66
|
+
Queues are defined in: `src/main/resources/parameters/local/rabbitmq.yaml`
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
queues:
|
|
70
|
+
user-created: user.user-created
|
|
71
|
+
order-placed: order.order-placed
|
|
72
|
+
payment-processed: payment.payment-processed
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Tip:** Generate queues using:
|
|
76
|
+
```bash
|
|
77
|
+
eva generate rabbitmq-event <module> <event-name>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
### Example 1: Basic Listener Creation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Create listeners in 'notification' module
|
|
88
|
+
eva g rabbitmq-listener notification
|
|
89
|
+
|
|
90
|
+
# Select queues from interactive menu:
|
|
91
|
+
# ✓ user-created (user.user-created)
|
|
92
|
+
# ✓ order-placed (order.order-placed)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Generated:**
|
|
96
|
+
```
|
|
97
|
+
notification/infrastructure/rabbitListener/
|
|
98
|
+
├── NotificationUserCreatedListener.java
|
|
99
|
+
└── NotificationOrderPlacedListener.java
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Generated code for NotificationUserCreatedListener.java:**
|
|
103
|
+
```java
|
|
104
|
+
package com.example.notification.infrastructure.rabbitListener;
|
|
105
|
+
|
|
106
|
+
import com.example.shared.infrastructure.configurations.useCaseConfig.UseCaseMediator;
|
|
107
|
+
import com.example.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
108
|
+
|
|
109
|
+
import com.fasterxml.jackson.core.type.TypeReference;
|
|
110
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
111
|
+
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
112
|
+
import com.rabbitmq.client.Channel;
|
|
113
|
+
import org.springframework.amqp.core.Message;
|
|
114
|
+
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
115
|
+
import org.springframework.stereotype.Component;
|
|
116
|
+
import org.slf4j.Logger;
|
|
117
|
+
import org.slf4j.LoggerFactory;
|
|
118
|
+
|
|
119
|
+
import java.io.IOException;
|
|
120
|
+
import java.util.Map;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* RabbitMQ listener for queue user.user-created
|
|
124
|
+
*/
|
|
125
|
+
@Component("notificationUserCreatedListener")
|
|
126
|
+
public class NotificationUserCreatedListener {
|
|
127
|
+
|
|
128
|
+
private static final Logger log = LoggerFactory.getLogger(NotificationUserCreatedListener.class);
|
|
129
|
+
|
|
130
|
+
private final UseCaseMediator useCaseMediator;
|
|
131
|
+
private final ObjectMapper objectMapper;
|
|
132
|
+
|
|
133
|
+
public NotificationUserCreatedListener(UseCaseMediator useCaseMediator, ObjectMapper objectMapper) {
|
|
134
|
+
this.useCaseMediator = useCaseMediator;
|
|
135
|
+
this.objectMapper = objectMapper;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@RabbitListener(queues = "${queues.user-created}")
|
|
139
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
140
|
+
long deliveryTag = message.getMessageProperties().getDeliveryTag();
|
|
141
|
+
|
|
142
|
+
EventEnvelope<Map<String, Object>> event;
|
|
143
|
+
try {
|
|
144
|
+
event = objectMapper.readValue(
|
|
145
|
+
message.getBody(),
|
|
146
|
+
new TypeReference<EventEnvelope<Map<String, Object>>>() {});
|
|
147
|
+
} catch (JsonProcessingException e) {
|
|
148
|
+
log.error("Fatal deserialization error — sending to DLQ: {}", e.getMessage());
|
|
149
|
+
channel.basicNack(deliveryTag, false, false);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// TODO: Implement event processing logic
|
|
154
|
+
// Example: useCaseMediator.dispatch(new YourCommand(event.data()));
|
|
155
|
+
|
|
156
|
+
channel.basicAck(deliveryTag, false);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Example 2: Adding More Listeners
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Run command again to add more listeners
|
|
168
|
+
eva g rabbitmq-listener order
|
|
169
|
+
|
|
170
|
+
# Select additional queues:
|
|
171
|
+
# ✓ payment-processed (payment.payment-processed)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Result:** New listener class is **created independently** — existing listeners are not modified:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
order/infrastructure/rabbitListener/
|
|
178
|
+
└── OrderPaymentProcessedListener.java
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Example 3: Multiple Modules Subscribing to Same Event
|
|
184
|
+
|
|
185
|
+
**Scenario:** Both `notification` and `analytics` modules want to consume `user-created` events.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# In notification module
|
|
189
|
+
eva g rabbitmq-listener notification
|
|
190
|
+
# Select: user-created → NotificationUserCreatedListener.java
|
|
191
|
+
|
|
192
|
+
# In analytics module
|
|
193
|
+
eva g rabbitmq-listener analytics
|
|
194
|
+
# Select: user-created → AnalyticsUserCreatedListener.java
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Result:** No bean name conflicts — each module has its own listener with a module-prefixed class and bean name.
|
|
198
|
+
|
|
199
|
+
> **Note:** In RabbitMQ, each consumer queue is independent. If both modules need to receive **all** messages, each should have its own queue bound to the producer's exchange (configured via `rabbitmq.yaml`).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Example 4: Processing Events with Use Cases
|
|
204
|
+
|
|
205
|
+
**Implement event processing in generated listener:**
|
|
206
|
+
|
|
207
|
+
```java
|
|
208
|
+
@RabbitListener(queues = "${queues.user-created}")
|
|
209
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
210
|
+
long deliveryTag = message.getMessageProperties().getDeliveryTag();
|
|
211
|
+
|
|
212
|
+
EventEnvelope<Map<String, Object>> event;
|
|
213
|
+
try {
|
|
214
|
+
event = objectMapper.readValue(
|
|
215
|
+
message.getBody(),
|
|
216
|
+
new TypeReference<EventEnvelope<Map<String, Object>>>() {});
|
|
217
|
+
} catch (JsonProcessingException e) {
|
|
218
|
+
log.error("Fatal deserialization error — sending to DLQ: {}", e.getMessage());
|
|
219
|
+
channel.basicNack(deliveryTag, false, false);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
String userId = (String) event.data().get("userId");
|
|
225
|
+
String email = (String) event.data().get("email");
|
|
226
|
+
|
|
227
|
+
useCaseMediator.dispatch(new SendWelcomeEmailCommand(userId, email));
|
|
228
|
+
|
|
229
|
+
channel.basicAck(deliveryTag, false);
|
|
230
|
+
} catch (Exception e) {
|
|
231
|
+
log.error("Processing failed — sending to DLQ: {}", e.getMessage());
|
|
232
|
+
channel.basicNack(deliveryTag, false, false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Generated Structure
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
<module>/
|
|
243
|
+
└── infrastructure/
|
|
244
|
+
└── rabbitListener/
|
|
245
|
+
├── <Module><Topic>Listener.java # Individual listener per queue
|
|
246
|
+
├── NotificationUserCreatedListener.java # Notification module listening to user-created
|
|
247
|
+
├── OrderOrderPlacedListener.java # Order module listening to order-placed
|
|
248
|
+
└── AnalyticsUserCreatedListener.java # Analytics module listening to user-created
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Class Naming Pattern:** `{ModuleName}{QueueName}Listener`
|
|
252
|
+
|
|
253
|
+
| Module | Queue key | Class name |
|
|
254
|
+
|--------|-----------|------------|
|
|
255
|
+
| `notification` | `user-created` | `NotificationUserCreatedListener` |
|
|
256
|
+
| `order` | `order-placed` | `OrderOrderPlacedListener` |
|
|
257
|
+
| `analytics` | `user-created` | `AnalyticsUserCreatedListener` |
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Configuration
|
|
262
|
+
|
|
263
|
+
### rabbitmq.yaml Structure
|
|
264
|
+
|
|
265
|
+
Location: `src/main/resources/parameters/local/rabbitmq.yaml`
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
spring:
|
|
269
|
+
rabbitmq:
|
|
270
|
+
host: localhost
|
|
271
|
+
port: 5672
|
|
272
|
+
username: guest
|
|
273
|
+
password: guest
|
|
274
|
+
virtual-host: /
|
|
275
|
+
listener:
|
|
276
|
+
simple:
|
|
277
|
+
acknowledge-mode: manual
|
|
278
|
+
concurrency: 3
|
|
279
|
+
retry:
|
|
280
|
+
enabled: true
|
|
281
|
+
max-attempts: 3
|
|
282
|
+
initial-interval: 1500
|
|
283
|
+
|
|
284
|
+
exchanges:
|
|
285
|
+
user: user.events
|
|
286
|
+
order: order.events
|
|
287
|
+
|
|
288
|
+
queues:
|
|
289
|
+
user-created: user.user-created
|
|
290
|
+
order-placed: order.order-placed
|
|
291
|
+
|
|
292
|
+
routing-keys:
|
|
293
|
+
user-created: user.created
|
|
294
|
+
order-placed: order.placed
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Environment-Specific Configuration
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
# parameters/local/rabbitmq.yaml
|
|
301
|
+
spring.rabbitmq.host: localhost
|
|
302
|
+
|
|
303
|
+
# parameters/production/rabbitmq.yaml
|
|
304
|
+
spring.rabbitmq.host: ${RABBITMQ_HOST}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## How It Works
|
|
310
|
+
|
|
311
|
+
### 1. Command Execution Flow
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
User runs command
|
|
315
|
+
↓
|
|
316
|
+
Validates eva project
|
|
317
|
+
↓
|
|
318
|
+
Checks RabbitMQ client installed
|
|
319
|
+
↓
|
|
320
|
+
Validates module exists
|
|
321
|
+
↓
|
|
322
|
+
Reads available queues from rabbitmq.yaml
|
|
323
|
+
↓
|
|
324
|
+
Prompts user to select queues (multi-select)
|
|
325
|
+
↓
|
|
326
|
+
For each selected queue:
|
|
327
|
+
├── Generate listener class name (e.g., NotificationUserCreated = Notification + UserCreated)
|
|
328
|
+
├── Generate bean name (e.g., notificationUserCreatedListener)
|
|
329
|
+
├── Check if listener class already exists
|
|
330
|
+
│ ├── YES → Skip (show warning)
|
|
331
|
+
│ └── NO → Create new listener class
|
|
332
|
+
└── Generate handle() method with @RabbitListener + manual ack
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 2. Runtime Event Processing Flow
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
RabbitMQ queue receives message
|
|
339
|
+
↓
|
|
340
|
+
Spring AMQP delivers raw Message to listener
|
|
341
|
+
↓
|
|
342
|
+
ObjectMapper deserializes body to EventEnvelope<Map<String, Object>>
|
|
343
|
+
├── Deserialization fails → basicNack (→ DLQ)
|
|
344
|
+
└── Success → continue
|
|
345
|
+
↓
|
|
346
|
+
Extract data from event.data()
|
|
347
|
+
↓
|
|
348
|
+
Create command/query object
|
|
349
|
+
↓
|
|
350
|
+
Dispatch to UseCaseMediator
|
|
351
|
+
↓
|
|
352
|
+
Use case processes event
|
|
353
|
+
↓
|
|
354
|
+
Call channel.basicAck() to acknowledge
|
|
355
|
+
↓ (on error)
|
|
356
|
+
Call channel.basicNack(deliveryTag, false, false) → message goes to DLQ
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Error Handling
|
|
362
|
+
|
|
363
|
+
### Manual Acknowledgment
|
|
364
|
+
|
|
365
|
+
Unlike Kafka (which uses `Acknowledgment.acknowledge()`), RabbitMQ listeners use **channel-level acknowledgment**:
|
|
366
|
+
|
|
367
|
+
```java
|
|
368
|
+
// Success — remove from queue
|
|
369
|
+
channel.basicAck(deliveryTag, false);
|
|
370
|
+
|
|
371
|
+
// Failure — send to DLQ (do NOT requeue)
|
|
372
|
+
channel.basicNack(deliveryTag, false, false);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Deserialization Errors
|
|
376
|
+
|
|
377
|
+
Fatal deserialization errors (malformed JSON) are immediately nack'd to the DLQ — no retry:
|
|
378
|
+
|
|
379
|
+
```java
|
|
380
|
+
} catch (JsonProcessingException e) {
|
|
381
|
+
log.error("Fatal deserialization error — sending to DLQ: {}", e.getMessage());
|
|
382
|
+
channel.basicNack(deliveryTag, false, false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Dead-Letter Queue (DLQ)
|
|
388
|
+
|
|
389
|
+
Each queue has a companion `.dlq` queue. Messages that are nack'd arrive in the DLQ for inspection:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
[user.user-created] → nack → [user.user-created.dlq]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Monitor DLQ in the RabbitMQ Management UI: `http://localhost:15672`
|
|
396
|
+
|
|
397
|
+
### Retry Configuration
|
|
398
|
+
|
|
399
|
+
Retry is configured at the Spring AMQP level in `rabbitmq.yaml`:
|
|
400
|
+
|
|
401
|
+
```yaml
|
|
402
|
+
spring.rabbitmq.listener.simple.retry:
|
|
403
|
+
enabled: true
|
|
404
|
+
max-attempts: 3
|
|
405
|
+
initial-interval: 1500
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Best Practices
|
|
411
|
+
|
|
412
|
+
### ✅ DO
|
|
413
|
+
|
|
414
|
+
1. **Always acknowledge after successful processing**
|
|
415
|
+
```java
|
|
416
|
+
channel.basicAck(deliveryTag, false);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
2. **Nack to DLQ on unrecoverable errors**
|
|
420
|
+
```java
|
|
421
|
+
channel.basicNack(deliveryTag, false, false); // requeue=false → DLQ
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
3. **Delegate to use cases via UseCaseMediator**
|
|
425
|
+
```java
|
|
426
|
+
useCaseMediator.dispatch(new ProcessOrderCommand(data));
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
4. **Handle deserialization separately** from business logic errors
|
|
430
|
+
|
|
431
|
+
5. **Use idempotent operations** — same message processed multiple times = same result
|
|
432
|
+
|
|
433
|
+
### ❌ DON'T
|
|
434
|
+
|
|
435
|
+
1. **Don't acknowledge before processing**
|
|
436
|
+
```java
|
|
437
|
+
channel.basicAck(deliveryTag, false); // ❌ Don't do this first
|
|
438
|
+
processEvent(event); // If this fails, message is lost
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
2. **Don't requeue indefinitely**
|
|
442
|
+
```java
|
|
443
|
+
channel.basicNack(deliveryTag, false, true); // ❌ requeue=true creates infinite loop
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
3. **Don't catch and swallow exceptions**
|
|
447
|
+
```java
|
|
448
|
+
try {
|
|
449
|
+
processEvent(event);
|
|
450
|
+
} catch (Exception e) {
|
|
451
|
+
e.printStackTrace();
|
|
452
|
+
channel.basicAck(deliveryTag, false); // ❌ Message lost on error
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
4. **Each listener class handles one queue** — Single Responsibility Principle
|
|
457
|
+
|
|
458
|
+
5. **Add new listeners without modifying existing code** — Open/Closed Principle
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Comparison with Kafka Listeners
|
|
463
|
+
|
|
464
|
+
| Aspect | Kafka | RabbitMQ |
|
|
465
|
+
|--------|-------|----------|
|
|
466
|
+
| Annotation | `@KafkaListener(topics = ...)` | `@RabbitListener(queues = ...)` |
|
|
467
|
+
| Acknowledgment | `Acknowledgment.acknowledge()` | `channel.basicAck(tag, false)` |
|
|
468
|
+
| Error routing | Retry + no action | `channel.basicNack()` → DLQ |
|
|
469
|
+
| Deserialization | `EventEnvelope<Map>` automatic | `Message` body + manual `ObjectMapper` |
|
|
470
|
+
| Config file | `kafka.yaml` (`topics:`) | `rabbitmq.yaml` (`queues:`) |
|
|
471
|
+
| Listener directory | `kafkaListener/` | `rabbitListener/` |
|
|
472
|
+
| Class naming | `{Module}{Topic}Listener` | `{Module}{Queue}Listener` |
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Common Use Cases
|
|
477
|
+
|
|
478
|
+
### 1. Notification Service
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
eva g rabbitmq-listener notification
|
|
482
|
+
# ✓ user-created (send welcome email)
|
|
483
|
+
# ✓ order-placed (send order confirmation)
|
|
484
|
+
# ✓ payment-processed (send payment receipt)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### 2. Analytics/Audit Service
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
eva g rabbitmq-listener analytics
|
|
491
|
+
# ✓ user-created
|
|
492
|
+
# ✓ order-placed
|
|
493
|
+
# ✓ order-shipped
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 3. Saga Orchestration
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
eva g rabbitmq-listener order
|
|
500
|
+
# ✓ payment-processed (complete order)
|
|
501
|
+
# ✓ payment-failed (cancel order)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Standalone vs domain.yaml Listeners
|
|
507
|
+
|
|
508
|
+
This standalone command generates a **generic stub** listener with a `TODO` comment. For **fully typed** listeners with IntegrationEvent records, typed Commands, and CommandHandlers, use the `listeners[]` section in `domain.yaml` and run `eva g entities <module>`.
|
|
509
|
+
|
|
510
|
+
| Feature | Standalone (`g rabbitmq-listener`) | domain.yaml (`listeners[]`) |
|
|
511
|
+
|---------|------------------------------------|-----------------------------|
|
|
512
|
+
| Template | `RabbitListenerSimple.java.ejs` | `RabbitListenerClass.java.ejs` |
|
|
513
|
+
| Typed fields | No — generic `Map<String, Object>` | Yes — explicit field extraction |
|
|
514
|
+
| IntegrationEvent | Not generated | Generated |
|
|
515
|
+
| Command + Handler | Not generated | Generated |
|
|
516
|
+
| Infrastructure beans | Not generated | Generated (exchange + queue + binding) |
|
|
517
|
+
| Use case | Manual implementation | Scaffold with CommandHandler |
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Troubleshooting
|
|
522
|
+
|
|
523
|
+
### ❌ "RabbitMQ client is not installed"
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
eva add rabbitmq-client
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### ❌ "No queues found in rabbitmq.yaml"
|
|
530
|
+
|
|
531
|
+
Generate queues first:
|
|
532
|
+
```bash
|
|
533
|
+
eva g rabbitmq-event user user-created
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Or manually add to `rabbitmq.yaml`:
|
|
537
|
+
```yaml
|
|
538
|
+
queues:
|
|
539
|
+
user-created: user.user-created
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### ❌ "Module not found"
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
eva add module <module-name>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### ❌ Messages not being consumed
|
|
549
|
+
|
|
550
|
+
**Checklist:**
|
|
551
|
+
1. RabbitMQ server is running (`docker-compose up -d`)
|
|
552
|
+
2. Queue exists in RabbitMQ (check Management UI: `http://localhost:15672`)
|
|
553
|
+
3. Queue name in `rabbitmq.yaml` matches actual RabbitMQ queue
|
|
554
|
+
4. No deserialization errors in application logs
|
|
555
|
+
5. Check DLQ for nack'd messages
|
|
556
|
+
|
|
557
|
+
### ❌ Duplicate message processing
|
|
558
|
+
|
|
559
|
+
Ensure idempotent operations or use message deduplication:
|
|
560
|
+
|
|
561
|
+
```java
|
|
562
|
+
@RabbitListener(queues = "${queues.user-created}")
|
|
563
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
564
|
+
// ...
|
|
565
|
+
String eventId = (String) event.data().get("eventId");
|
|
566
|
+
if (eventRepository.existsByEventId(eventId)) {
|
|
567
|
+
channel.basicAck(deliveryTag, false); // Skip duplicate
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
processEvent(event);
|
|
571
|
+
eventRepository.save(new ProcessedEvent(eventId));
|
|
572
|
+
channel.basicAck(deliveryTag, false);
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Next Steps After Generation
|
|
579
|
+
|
|
580
|
+
1. **Implement processing logic** in generated `handle()` method
|
|
581
|
+
2. **Create use cases** for event processing
|
|
582
|
+
3. **Add error handling** with proper nack → DLQ routing
|
|
583
|
+
4. **Monitor DLQ** via RabbitMQ Management UI
|
|
584
|
+
5. **Write integration tests** with `@RabbitIntegrationTest` or Testcontainers
|
|
585
|
+
6. **Configure concurrency** for scaling (`spring.rabbitmq.listener.simple.concurrency`)
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Related Commands
|
|
590
|
+
|
|
591
|
+
- [`generate rabbitmq-event`](./GENERATE_RABBITMQ_EVENT.md) — Create RabbitMQ event publisher
|
|
592
|
+
- [`generate kafka-listener`](./GENERATE_KAFKA_LISTENER.md) — Kafka equivalent of this command
|
|
593
|
+
- [`generate usecase`](./GENERATE_USECASE.md) — Create use cases to process events
|
|
594
|
+
- [`add module`](./ADD_MODULE.md) — Create new module
|
|
595
|
+
- [RabbitMQ Production Config](../RABBITMQ_PRODUCTION_CONFIG.md) — Production-ready configuration reference
|
|
@@ -36,7 +36,10 @@ eva g temporal-flow order
|
|
|
36
36
|
- `application/usecases/ProcessOrderWorkFlow.java` — `@WorkflowInterface`
|
|
37
37
|
- `application/usecases/ProcessOrderWorkFlowImpl.java` — implementation with Saga
|
|
38
38
|
- `application/usecases/ProcessOrderWorkFlowService.java` — Spring service facade
|
|
39
|
-
-
|
|
39
|
+
- `domain/interfaces/OrderHeavyActivity.java` — module-scoped marker interface
|
|
40
|
+
- `domain/interfaces/OrderLightActivity.java` — module-scoped marker interface
|
|
41
|
+
- `infrastructure/configurations/OrderTemporalWorkerConfig.java` — module worker registration
|
|
42
|
+
- Appends `ORDER` queue section to `temporal.yaml`
|
|
40
43
|
|
|
41
44
|
### Example 2: Payment workflow
|
|
42
45
|
|
|
@@ -49,7 +52,10 @@ eva g temporal-flow payment
|
|
|
49
52
|
- `application/usecases/ProcessPaymentWorkFlow.java`
|
|
50
53
|
- `application/usecases/ProcessPaymentWorkFlowImpl.java`
|
|
51
54
|
- `application/usecases/ProcessPaymentWorkFlowService.java`
|
|
52
|
-
-
|
|
55
|
+
- `domain/interfaces/PaymentHeavyActivity.java`
|
|
56
|
+
- `domain/interfaces/PaymentLightActivity.java`
|
|
57
|
+
- `infrastructure/configurations/PaymentTemporalWorkerConfig.java`
|
|
58
|
+
- Appends `PAYMENT` queue section to `temporal.yaml`
|
|
53
59
|
|
|
54
60
|
### Example 3: Multiple workflows in the same module
|
|
55
61
|
|
|
@@ -63,7 +69,7 @@ eva g temporal-flow order
|
|
|
63
69
|
# refund-order → generates RefundOrder files
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
Each run appends a new `registerWorkflowImplementationTypes(...)` entry to `
|
|
72
|
+
Each run appends a new `registerWorkflowImplementationTypes(...)` entry to `OrderTemporalWorkerConfig.java` without duplicating existing registrations.
|
|
67
73
|
|
|
68
74
|
## 📦 Generated Code Structure
|
|
69
75
|
|
|
@@ -113,10 +119,10 @@ public class ProcessOrderWorkFlowImpl implements ProcessOrderWorkFlow {
|
|
|
113
119
|
|
|
114
120
|
private Saga saga = new Saga(sagaOptions);
|
|
115
121
|
|
|
116
|
-
// Light activities (<30 s) — routed to
|
|
122
|
+
// Light activities (<30 s) — routed to ORDER_LIGHT_TASK_QUEUE
|
|
117
123
|
private final ActivityOptions lightActivityOptions = ActivityOptions.newBuilder()
|
|
118
124
|
.setStartToCloseTimeout(Duration.ofSeconds(30))
|
|
119
|
-
.setTaskQueue("
|
|
125
|
+
.setTaskQueue("ORDER_LIGHT_TASK_QUEUE")
|
|
120
126
|
.setRetryOptions(
|
|
121
127
|
RetryOptions.newBuilder()
|
|
122
128
|
.setMaximumAttempts(2)
|
|
@@ -126,10 +132,10 @@ public class ProcessOrderWorkFlowImpl implements ProcessOrderWorkFlow {
|
|
|
126
132
|
.build()
|
|
127
133
|
).build();
|
|
128
134
|
|
|
129
|
-
// Heavy activities (up to 2 min) — routed to
|
|
135
|
+
// Heavy activities (up to 2 min) — routed to ORDER_HEAVY_TASK_QUEUE
|
|
130
136
|
private final ActivityOptions heavyActivityOptions = ActivityOptions.newBuilder()
|
|
131
137
|
.setStartToCloseTimeout(Duration.ofSeconds(120))
|
|
132
|
-
.setTaskQueue("
|
|
138
|
+
.setTaskQueue("ORDER_HEAVY_TASK_QUEUE")
|
|
133
139
|
.setRetryOptions(
|
|
134
140
|
RetryOptions.newBuilder()
|
|
135
141
|
.setMaximumAttempts(2)
|
|
@@ -179,7 +185,7 @@ public class ProcessOrderWorkFlowService {
|
|
|
179
185
|
|
|
180
186
|
private final WorkflowClient workflowClient;
|
|
181
187
|
|
|
182
|
-
@Value("${temporal.flow-queue}")
|
|
188
|
+
@Value("${temporal.modules.order.flow-queue}")
|
|
183
189
|
private String flowQueue;
|
|
184
190
|
|
|
185
191
|
// ... constructor injection
|
|
@@ -214,7 +220,7 @@ public class ProcessOrderWorkFlowService {
|
|
|
214
220
|
}
|
|
215
221
|
```
|
|
216
222
|
|
|
217
|
-
###
|
|
223
|
+
### OrderTemporalWorkerConfig.java — Auto-generated
|
|
218
224
|
|
|
219
225
|
```java
|
|
220
226
|
// registered automatically by eva g temporal-flow
|
|
@@ -223,11 +229,45 @@ workflowWorker.registerWorkflowImplementationTypes(ProcessOrderWorkFlowImpl.clas
|
|
|
223
229
|
|
|
224
230
|
## 🏗️ Queue Architecture
|
|
225
231
|
|
|
232
|
+
Queues are **module-scoped** — each module gets its own set of queues prefixed with the module name in SCREAMING_SNAKE_CASE:
|
|
233
|
+
|
|
226
234
|
| Queue | Purpose |
|
|
227
235
|
|-------|---------|
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
236
|
+
| `{MODULE}_WORKFLOW_QUEUE` | Workflow orchestration (WorkFlowImpl runs here) |
|
|
237
|
+
| `{MODULE}_LIGHT_TASK_QUEUE` | Fast activities (< 30 s), injected via `lightActivityOptions` |
|
|
238
|
+
| `{MODULE}_HEAVY_TASK_QUEUE` | Long-running activities (up to 2 min), injected via `heavyActivityOptions` |
|
|
239
|
+
|
|
240
|
+
For example, the `order` module generates:
|
|
241
|
+
- `ORDER_WORKFLOW_QUEUE`
|
|
242
|
+
- `ORDER_LIGHT_TASK_QUEUE`
|
|
243
|
+
- `ORDER_HEAVY_TASK_QUEUE`
|
|
244
|
+
|
|
245
|
+
The `payment` module generates:
|
|
246
|
+
- `PAYMENT_WORKFLOW_QUEUE`
|
|
247
|
+
- `PAYMENT_LIGHT_TASK_QUEUE`
|
|
248
|
+
- `PAYMENT_HEAVY_TASK_QUEUE`
|
|
249
|
+
|
|
250
|
+
This ensures each module's workers are isolated and can be scaled independently.
|
|
251
|
+
|
|
252
|
+
### temporal.yaml (auto-updated)
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
temporal:
|
|
256
|
+
service-url: localhost:7233
|
|
257
|
+
namespace: default
|
|
258
|
+
number-flow-worker: 10
|
|
259
|
+
number-heavy-worker: 10
|
|
260
|
+
number-light-worker: 10
|
|
261
|
+
modules:
|
|
262
|
+
order:
|
|
263
|
+
flow-queue: ORDER_WORKFLOW_QUEUE
|
|
264
|
+
heavy-queue: ORDER_HEAVY_TASK_QUEUE
|
|
265
|
+
light-queue: ORDER_LIGHT_TASK_QUEUE
|
|
266
|
+
payment:
|
|
267
|
+
flow-queue: PAYMENT_WORKFLOW_QUEUE
|
|
268
|
+
heavy-queue: PAYMENT_HEAVY_TASK_QUEUE
|
|
269
|
+
light-queue: PAYMENT_LIGHT_TASK_QUEUE
|
|
270
|
+
```
|
|
231
271
|
|
|
232
272
|
Activity stubs created inside the workflow must pass the matching options object — see [GENERATE_TEMPORAL_ACTIVITY.md](./GENERATE_TEMPORAL_ACTIVITY.md).
|
|
233
273
|
|