github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/components/requestreply/command_bus.go (about)

     1  package requestreply
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/pkg/errors"
     7  
     8  	"github.com/wfusion/gofusion/common/infra/watermill/message"
     9  	"github.com/wfusion/gofusion/common/utils"
    10  )
    11  
    12  type CommandBus interface {
    13  	SendWithModifiedMessage(ctx context.Context, cmd any, modify func(*message.Message) error) error
    14  }
    15  
    16  // SendWithReply sends command to the command bus and receives a replies of the command handler.
    17  // It returns a channel with replies, cancel function and error.
    18  // If more than one replies are sent, only the first which is received is returned.
    19  //
    20  // If you expect multiple replies, please use SendWithReplies instead.
    21  //
    22  // SendWithReply is blocking until the first reply is received or the context is canceled.
    23  // SendWithReply can be cancelled by cancelling context or
    24  // by exceeding the timeout set in the backend (if set).
    25  //
    26  // SendWithReply can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler).
    27  // If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type:
    28  //
    29  //	 reply, err := requestreply.SendWithReply[requestreply.NoResult](
    30  //			context.Background(),
    31  //			ts.CommandBus,
    32  //			ts.RequestReplyBackend,
    33  //			&TestCommand{ID: "1"},
    34  //		)
    35  //
    36  // If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type:
    37  //
    38  //	 reply, err := requestreply.SendWithReply[SomeTypeReturnedByHandler](
    39  //			context.Background(),
    40  //			ts.CommandBus,
    41  //			ts.RequestReplyBackend,
    42  //			&TestCommand{ID: "1"},
    43  //		)
    44  func SendWithReply[Result any](
    45  	ctx context.Context,
    46  	c CommandBus,
    47  	backend Backend[Result],
    48  	cmd any,
    49  ) (Reply[Result], error) {
    50  	replyCh, cancel, err := SendWithReplies[Result](ctx, c, backend, cmd)
    51  	if err != nil {
    52  		return Reply[Result]{}, errors.Wrap(err, "SendWithReplies failed")
    53  	}
    54  	defer cancel()
    55  
    56  	select {
    57  	case <-ctx.Done():
    58  		return Reply[Result]{}, errors.Wrap(ctx.Err(), "context closed")
    59  	case reply := <-replyCh:
    60  		return reply, nil
    61  	}
    62  }
    63  
    64  // SendWithReplies sends command to the command bus and receives a replies of the command handler.
    65  // It returns a channel with replies, cancel function and error.
    66  //
    67  // SendWithReplies can be cancelled by calling cancel function or by cancelling context or
    68  // When SendWithReplies is canceled, the returned channel is closed as well.
    69  // by exceeding the timeout set in the backend (if set).
    70  // Warning: It's important to cancel the function, because it's listening for the replies in the background.
    71  // Lack of cancelling the function can lead to subscriber leak.
    72  //
    73  // SendWithReplies can listen for handlers with results (NewCommandHandlerWithResult) and without results (NewCommandHandler).
    74  // If you are listening for handlers without results, you should pass `NoResult` or `struct{}` as `Result` generic type:
    75  //
    76  //	 replyCh, cancel, err := requestreply.SendWithReplies[requestreply.NoResult](
    77  //			context.Background(),
    78  //			ts.CommandBus,
    79  //			ts.RequestReplyBackend,
    80  //			&TestCommand{ID: "1"},
    81  //		)
    82  //
    83  // If `NewCommandHandlerWithResult` handler returns a specific type, you should pass it as `Result` generic type:
    84  //
    85  //	 replyCh, cancel, err := requestreply.SendWithReplies[SomeTypeReturnedByHandler](
    86  //			context.Background(),
    87  //			ts.CommandBus,
    88  //			ts.RequestReplyBackend,
    89  //			&TestCommand{ID: "1"},
    90  //		)
    91  //
    92  // SendWithReplies will send the replies to the channel until the context is cancelled or the timeout is exceeded.
    93  // They are multiple cases when more than one reply can be sent:
    94  //   - when the handler returns an error, and backend is configured to nack the message on error
    95  //     (for the PubSubBackend, it depends on `PubSubBackendConfig.AckCommandErrors` option.),
    96  //   - when you are using fan-out mechanism and commands are handled multiple times,
    97  func SendWithReplies[Result any](
    98  	ctx context.Context,
    99  	c CommandBus,
   100  	backend Backend[Result],
   101  	cmd any,
   102  ) (replCh <-chan Reply[Result], cancel func(), err error) {
   103  	ctx, cancel = context.WithCancel(ctx)
   104  
   105  	defer func() {
   106  		if err != nil {
   107  			cancel()
   108  		}
   109  	}()
   110  
   111  	operationID := utils.UUID()
   112  
   113  	replyChan, err := backend.ListenForNotifications(ctx, BackendListenForNotificationsParams{
   114  		Command:     cmd,
   115  		OperationID: OperationID(operationID),
   116  	})
   117  	if err != nil {
   118  		return nil, cancel, errors.Wrap(err, "cannot listen for reply")
   119  	}
   120  
   121  	if err := c.SendWithModifiedMessage(ctx, cmd, func(m *message.Message) error {
   122  		m.Metadata.Set(OperationIDMetadataKey, operationID)
   123  		return nil
   124  	}); err != nil {
   125  		return nil, cancel, errors.Wrap(err, "cannot send command")
   126  	}
   127  
   128  	return replyChan, cancel, nil
   129  }