1169 lines
41 KiB
HTML
1169 lines
41 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Watermill Consumer Groups Example</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
|
<link href="https://unpkg.com/@highlightjs/cdn-assets@11.7.0/styles/default.min.css" rel="stylesheet">
|
|
<style>
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<nav class="navbar bg-body-tertiary mb-5">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">Watermill Consumer Groups Example</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div id="root"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js"></script>
|
|
|
|
<script type="text/babel">
|
|
function App() {
|
|
const [chapters, setChapters] = React.useState([
|
|
{
|
|
number: 0,
|
|
title: "Consumer Groups",
|
|
description: `
|
|
<p>Consumer groups let you decide which handler receives a message.</p>
|
|
<p>They are related to subscribers.</p>
|
|
<p>Some Pub/Sub take a different approach, but the idea stays the same.</p>
|
|
<p>Follow the tabs above to see all examples in order.</p>
|
|
`,
|
|
services: [],
|
|
code: [],
|
|
},
|
|
{
|
|
number: 1,
|
|
title: "Signing up for the newsletter",
|
|
description: `
|
|
<p>When a user signs up, a <span class="badge bg-dark">UserSignedUp</span> event is published.</p>
|
|
|
|
<p>In the simplest scenario, there's a single service with one handler subscribed to this event.
|
|
It's adds the user to the newsletter list.
|
|
</p>
|
|
|
|
<p>Flip the switch below to start sending messages and see what happens.</p>
|
|
`,
|
|
topic: "UserSignedUp-1",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: null,
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
}
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `router.AddNoPublisherHandler(
|
|
"OnUserSignedUp",
|
|
"UserSignedUp",
|
|
subscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 2,
|
|
title: "Introducing the CRM service",
|
|
description: `
|
|
<p>We add one more service. It adds the signed up users to the CRM software.</p>
|
|
<p>Handlers in both services receive the same message.</p>
|
|
`,
|
|
topic: "UserSignedUp-2",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: null,
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "crm-service-1",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: null,
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
}
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `router.AddNoPublisherHandler(
|
|
"OnUserSignedUp",
|
|
"UserSignedUp",
|
|
subscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)`,
|
|
}, {
|
|
service: 'crm-service',
|
|
code: `router.AddNoPublisherHandler(
|
|
"OnUserSignedUp",
|
|
"UserSignedUp",
|
|
subscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the CRM")
|
|
|
|
return nil
|
|
},
|
|
)`,
|
|
}],
|
|
},
|
|
{
|
|
number: 3,
|
|
topic: "UserSignedUp-3",
|
|
title: "Two replicas of the newsletter service",
|
|
description: `
|
|
<p>We scale the newsletter service horizontally, adding an identical replica.</p>
|
|
|
|
<p>This can be done for performance or availability reasons.</p>
|
|
|
|
<div class="alert alert-warning" role="alert">
|
|
<strong>Anti-pattern:</strong> each message is processed twice.
|
|
</div>
|
|
`,
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: null,
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "newsletter-service-2",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: null,
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `router.AddNoPublisherHandler(
|
|
"OnUserSignedUp",
|
|
"UserSignedUp",
|
|
subscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 4,
|
|
title: "Introducing a consumer group",
|
|
description: `
|
|
<p>Introducing a consumer group makes messages delivered in a round-robin fashion.</p>
|
|
<p>Each message is processed only once. It is delivered to the group, and the Pub/Sub decides which subscriber receives it.</p>
|
|
|
|
<p>The consumer group is a string.</p>
|
|
<p>Some Pub/Subs use different primitives to achieve the same thing (e.g., AMQP).</p>
|
|
`,
|
|
topic: "UserSignedUp-4",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: {
|
|
name: "newsletter-service",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "newsletter-service-2",
|
|
handlers: [
|
|
{
|
|
name: "OnUserSignedUp",
|
|
group: {
|
|
name: "newsletter-service",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `newsletterServiceGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "newsletter-service",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"OnUserSignedUp",
|
|
"UserSignedUp",
|
|
newsletterServiceGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 5,
|
|
title: "Adding a second handler to the newsletter service",
|
|
description: `
|
|
<p>We now have two newsletter lists: one for promotions, and one for news.</p>
|
|
|
|
<p>We add another handler subscribed to the same event.</p>
|
|
|
|
<p>Note: we changed the first handler's name.</p>
|
|
|
|
<p>We keep the same consumer groups.</p>
|
|
|
|
<div class="alert alert-warning" role="alert">
|
|
<strong>Anti-pattern:</strong> every other message is delivered to a different handler.
|
|
</div>
|
|
`,
|
|
topic: "UserSignedUp-5",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "newsletter-service",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "newsletter-service",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `newsletterServiceGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "newsletter-service",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToPromotionsList",
|
|
"UserSignedUp",
|
|
newsletterServiceGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToNewsList",
|
|
"UserSignedUp",
|
|
newsletterServiceGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.News {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the news list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 6,
|
|
title: "Changing the consumer group to the handler name",
|
|
description: `
|
|
<p>Introducing a consumer group per handler makes messages delivered to both handlers.</p>
|
|
`,
|
|
topic: "UserSignedUp-6",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `addToPromotionsListGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "AddToPromotionsList",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
addToNewsListGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "AddToNewsList",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToPromotionsList",
|
|
"UserSignedUp",
|
|
addToPromotionsListGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToNewsList",
|
|
"UserSignedUp",
|
|
addToNewsListGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.News {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the news list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 7,
|
|
title: "Two replicas with two handlers",
|
|
description: `
|
|
<p>We keep one consumer group per handler.</p>
|
|
<p>Events are delivered to each handler in a round-robin fashion.</p>
|
|
`,
|
|
topic: "UserSignedUp-7",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "newsletter-service-2",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `addToPromotionsListGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "AddToPromotionsList",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
addToNewsListGroupSubscriber, err := redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: "AddToNewsList",
|
|
},
|
|
logger,
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToPromotionsList",
|
|
"UserSignedUp",
|
|
addToPromotionsListGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
router.AddNoPublisherHandler(
|
|
"AddToNewsList",
|
|
"UserSignedUp",
|
|
addToNewsListGroupSubscriber,
|
|
func(msg *message.Message) error {
|
|
var event common.UserSignedUp
|
|
err := json.Unmarshal(msg.Payload, &event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !event.Consents.News {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", event.UserID, "to the news list")
|
|
|
|
return nil
|
|
},
|
|
)`
|
|
}],
|
|
},
|
|
{
|
|
number: 8,
|
|
title: "Adding a second handler to the CRM service",
|
|
description: `
|
|
<p>The CRM service now has one more responsibility: adding users to the Support software.</p>
|
|
|
|
<p>Handler names as consumer groups still make sense. However, a good practice is to include the service name as well.</p>
|
|
|
|
<p>Thanks to this, there's no risk of conflicts of the handler names between services.</p>
|
|
|
|
<p>Note: the replica number is not part of the service name!</p>
|
|
|
|
<p>This example features Watermill's CQRS component for easier creation of subscribers.</p>
|
|
`,
|
|
topic: "UserSignedUp-8",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "newsletter-service_AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "newsletter-service_AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "crm-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToCRM",
|
|
group: {
|
|
name: "crm-service_AddToCRM",
|
|
color: "danger",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToSupport",
|
|
group: {
|
|
name: "crm-service_AddToSupport",
|
|
color: "info",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: 'newsletter-service',
|
|
code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return params.EventName, nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: fmt.Sprintf("%s_%s", serviceName, params.HandlerName),
|
|
},
|
|
logger,
|
|
)
|
|
},
|
|
Marshaler: cqrs.JSONMarshaler{
|
|
GenerateName: cqrs.StructName,
|
|
},
|
|
Logger: logger,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = eventProc.AddHandlers(
|
|
cqrs.NewEventHandler("AddToPromotionsList", HandlePromotions),
|
|
cqrs.NewEventHandler("AddToNewsList", HandleNews),
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
func HandleNews(ctx context.Context, e *common.UserSignedUp) error {
|
|
if !e.Consents.News {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", e.UserID, "to the news list")
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandlePromotions(ctx context.Context, e *common.UserSignedUp) error {
|
|
if !e.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", e.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
}`,
|
|
}, {
|
|
service: 'crm-service',
|
|
code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return params.EventName, nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: fmt.Sprintf("%s_%s", serviceName, params.HandlerName),
|
|
},
|
|
logger,
|
|
)
|
|
},
|
|
Marshaler: cqrs.JSONMarshaler{
|
|
GenerateName: cqrs.StructName,
|
|
},
|
|
Logger: logger,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = eventProc.AddHandlers(
|
|
cqrs.NewEventHandler("AddToCRM", HandleCRM),
|
|
cqrs.NewEventHandler("AddToSupport", HandleSupport),
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
func HandleCRM(ctx context.Context, e *common.UserSignedUp) error {
|
|
fmt.Println("Adding user", e.UserID, "to the CRM")
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandleSupport(ctx context.Context, e *common.UserSignedUp) error {
|
|
fmt.Println("Adding user", e.UserID, "to the support channel")
|
|
|
|
return nil
|
|
}`
|
|
}],
|
|
},
|
|
{
|
|
number: 9,
|
|
title: "Running two replicas of both services",
|
|
description: `
|
|
<p>This example is identical as the previous one except both services run two replicas.</p>
|
|
`,
|
|
topic: "UserSignedUp-9",
|
|
services: [
|
|
{
|
|
name: "newsletter-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "newsletter-service_AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "newsletter-service_AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "crm-service-1",
|
|
handlers: [
|
|
{
|
|
name: "AddToCRM",
|
|
group: {
|
|
name: "crm-service_AddToCRM",
|
|
color: "danger",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToSupport",
|
|
group: {
|
|
name: "crm-service_AddToSupport",
|
|
color: "info",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "newsletter-service-2",
|
|
handlers: [
|
|
{
|
|
name: "AddToPromotionsList",
|
|
group: {
|
|
name: "newsletter-service_AddToPromotionsList",
|
|
color: "primary",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToNewsList",
|
|
group: {
|
|
name: "newsletter-service_AddToNewsList",
|
|
color: "warning",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: "crm-service-2",
|
|
handlers: [
|
|
{
|
|
name: "AddToCRM",
|
|
group: {
|
|
name: "crm-service_AddToCRM",
|
|
color: "danger",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
},
|
|
{
|
|
name: "AddToSupport",
|
|
group: {
|
|
name: "crm-service_AddToSupport",
|
|
color: "info",
|
|
},
|
|
received_message_id: "",
|
|
received_message_at: 0,
|
|
}
|
|
]
|
|
},
|
|
],
|
|
code: [{
|
|
service: "newsletter-service",
|
|
code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return params.EventName, nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: fmt.Sprintf("%s_%s", serviceName, params.HandlerName),
|
|
},
|
|
logger,
|
|
)
|
|
},
|
|
Marshaler: cqrs.JSONMarshaler{
|
|
GenerateName: cqrs.StructName,
|
|
},
|
|
Logger: logger,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = eventProc.AddHandlers(
|
|
cqrs.NewEventHandler("AddToPromotionsList", HandlePromotions),
|
|
cqrs.NewEventHandler("AddToNewsList", HandleNews),
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
func HandleNews(ctx context.Context, e *common.UserSignedUp) error {
|
|
if !e.Consents.News {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", e.UserID, "to the news list")
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandlePromotions(ctx context.Context, e *common.UserSignedUp) error {
|
|
if !e.Consents.Marketing {
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Adding user", e.UserID, "to the promotions list")
|
|
|
|
return nil
|
|
}`,
|
|
}, {
|
|
service: "crm-service",
|
|
code: `eventProc, err := cqrs.NewEventProcessorWithConfig(router, cqrs.EventProcessorConfig{
|
|
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
|
|
return params.EventName, nil
|
|
},
|
|
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
|
return redisstream.NewSubscriber(
|
|
redisstream.SubscriberConfig{
|
|
Client: subClient,
|
|
ConsumerGroup: fmt.Sprintf("%s_%s", serviceName, params.HandlerName),
|
|
},
|
|
logger,
|
|
)
|
|
},
|
|
Marshaler: cqrs.JSONMarshaler{
|
|
GenerateName: cqrs.StructName,
|
|
},
|
|
Logger: logger,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = eventProc.AddHandlers(
|
|
cqrs.NewEventHandler("AddToCRM", HandleCRM),
|
|
cqrs.NewEventHandler("AddToSupport", HandleSupport),
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
func HandleCRM(ctx context.Context, e *common.UserSignedUp) error {
|
|
fmt.Println("Adding user", e.UserID, "to the CRM")
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandleSupport(ctx context.Context, e *common.UserSignedUp) error {
|
|
fmt.Println("Adding user", e.UserID, "to the support channel")
|
|
|
|
return nil
|
|
}`
|
|
}],
|
|
},
|
|
]);
|
|
|
|
React.useEffect(() => {
|
|
const es = new EventSource("/api/messages");
|
|
es.addEventListener('data', event => {
|
|
console.log(event.data);
|
|
const data = JSON.parse(event.data);
|
|
|
|
let newChapters = [...chapters];
|
|
|
|
const now = Date.now();
|
|
|
|
for (let message of data) {
|
|
for (let i = 0; i < newChapters.length; i++) {
|
|
const chapter = newChapters[i];
|
|
if (chapter.topic === message.topic) {
|
|
for (let j = 0; j < chapter.services.length; j++) {
|
|
const service = chapter.services[j];
|
|
if (service.name === message.service) {
|
|
for (let k = 0; k < service.handlers.length; k++) {
|
|
const handler = service.handlers[k];
|
|
if (handler.name === message.handler) {
|
|
handler.received_message_id = message.id;
|
|
handler.received_message_at = now;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
setChapters(newChapters);
|
|
}, false);
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
let newChapters = [...chapters];
|
|
|
|
const now = Date.now();
|
|
|
|
for (let i = 0; i < newChapters.length; i++) {
|
|
const chapter = newChapters[i];
|
|
for (let j = 0; j < chapter.services.length; j++) {
|
|
const service = chapter.services[j];
|
|
for (let k = 0; k < service.handlers.length; k++) {
|
|
const handler = service.handlers[k];
|
|
if (now - handler.received_message_at > 1200) {
|
|
handler.received_message_at = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setChapters(newChapters);
|
|
}, 100);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="container">
|
|
<ul className="nav nav-tabs" role="tablist" style={{borderBottom: 0}}>
|
|
{chapters.map(chapter => (
|
|
<li key={chapter.number} className="nav-item mx-1" role="presentation">
|
|
<button
|
|
className={`nav-link ${chapter.number === 0 ? "active" : ""}`}
|
|
id={"example-tab-" + chapter.number }
|
|
data-bs-toggle="tab"
|
|
data-bs-target={"#tab-pane-" + chapter.number}
|
|
type="button"
|
|
role="tab"
|
|
aria-controls={"tab-pane-" + chapter.number}
|
|
aria-selected={chapter.number === 0 ? "true" : "false"}><span className="px-1">{chapter.number}</span></button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div className="tab-content">
|
|
{chapters.map(chapter => <Chapter key={chapter.number} chapter={chapter} />)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Chapter(props) {
|
|
const [sending, setSending] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
if (!sending) {
|
|
return;
|
|
}
|
|
fetch('/api/messages/' + props.chapter.topic, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
}
|
|
});
|
|
}, 1500);
|
|
return () => clearInterval(interval);
|
|
}, [sending]);
|
|
|
|
const columns = 2;
|
|
const rows = props.chapter.services.map((service, i) => {
|
|
return <Service key={i} service={service} />
|
|
}).reduce((r, element, index) => {
|
|
if (index % columns === 0) {
|
|
r.push([]);
|
|
}
|
|
r[r.length - 1].push(element);
|
|
return r;
|
|
}, []).map((row, i) => {
|
|
return (
|
|
<div key={i} className="row my-3">
|
|
{row}
|
|
</div>
|
|
);
|
|
})
|
|
|
|
React.useEffect(() => {
|
|
hljs.highlightAll();
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
className={`tab-pane fade ${props.chapter.number === 0 ? " show active" : ""}`}
|
|
id={"tab-pane-" + props.chapter.number}
|
|
role="tabpanel"
|
|
aria-labelledby={"tab-" + props.chapter.number}
|
|
tabIndex="0"
|
|
>
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h3>{props.chapter.number}. {props.chapter.title}</h3>
|
|
</div>
|
|
<div className="card-body">
|
|
<p dangerouslySetInnerHTML={{__html: props.chapter.description}}></p>
|
|
|
|
{props.chapter.services.length > 0 &&
|
|
<div className="form-check form-switch mx-3 mt-5 mb-4">
|
|
<input className="form-check-input" onChange={() => setSending(!sending) } type="checkbox" role="switch" id={`flexSwitchCheckDefault-${props.chapter.number}`} />
|
|
<label className="form-check-label" htmlFor={`flexSwitchCheckDefault-${props.chapter.number}`}>Send <span className="badge bg-dark">UserSignedUp</span> Events</label>
|
|
{sending && <div className="mx-3 spinner-grow spinner-grow-sm text-success" role="status"></div>}
|
|
</div>
|
|
}
|
|
|
|
<div className="container">
|
|
{rows}
|
|
</div>
|
|
|
|
<div className="accordion mt-5" id={"code" + props.chapter.number}>
|
|
{props.chapter.code.map(code => (
|
|
<div className="accordion-item">
|
|
<h2 className="accordion-header" id={"codeHeader" + props.chapter.number+code.service}>
|
|
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target={"#codeCollapse" + props.chapter.number+code.service} aria-expanded="false" aria-controls={"#codeCollapse" + props.chapter.number+code.service}>
|
|
Code: {code.service}
|
|
</button>
|
|
</h2>
|
|
<div id={"codeCollapse" + props.chapter.number + code.service} className="accordion-collapse collapse" aria-labelledby={"codeHeader"+props.chapter.number+code.service} data-bs-parent={"code"+props.chapter.number+code.service}>
|
|
<div className="accordion-body">
|
|
<pre>
|
|
<code className="language-go">
|
|
{code.code}
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Service(props) {
|
|
return (
|
|
<div className="col-6">
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h3>{props.service.name}</h3>
|
|
</div>
|
|
<ul className="list-group list-group-flush">
|
|
{props.service.handlers.map(handler => <Handler key={handler.name} handler={handler} />)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Handler(props) {
|
|
return (
|
|
<li className="list-group-item">
|
|
<h4>{props.handler.name}</h4>
|
|
{props.handler.group &&
|
|
<h4><span className={`badge text-bg-${props.handler.group.color}`}>Group: {props.handler.group.name}</span></h4>
|
|
}
|
|
<h1><span className={"badge " + (props.handler.received_message_at > 0 ? "text-bg-success" : "text-bg-light")}>{props.handler.received_message_id > 0 ? `Received #${props.handler.received_message_id}` : "Waiting for messages..."}</span></h1>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
const container = document.getElementById('root');
|
|
const root = ReactDOM.createRoot(container);
|
|
root.render(<App />);
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|