github.com/hashgraph/hedera-sdk-go/v2@v2.48.0/topic_message_query.go (about)

     1  package hedera
     2  
     3  /*-
     4   *
     5   * Hedera Go SDK
     6   *
     7   * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC
     8   *
     9   * Licensed under the Apache License, Version 2.0 (the "License");
    10   * you may not use this file except in compliance with the License.
    11   * You may obtain a copy of the License at
    12   *
    13   *      http://www.apache.org/licenses/LICENSE-2.0
    14   *
    15   * Unless required by applicable law or agreed to in writing, software
    16   * distributed under the License is distributed on an "AS IS" BASIS,
    17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    18   * See the License for the specific language governing permissions and
    19   * limitations under the License.
    20   *
    21   */
    22  
    23  import (
    24  	"context"
    25  	"io"
    26  	"math"
    27  	"regexp"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/hashgraph/hedera-protobufs-go/services"
    32  
    33  	"github.com/hashgraph/hedera-protobufs-go/mirror"
    34  	"google.golang.org/grpc/codes"
    35  	"google.golang.org/grpc/status"
    36  )
    37  
    38  var rstStream = regexp.MustCompile("(?i)\\brst[^0-9a-zA-Z]stream\\b") //nolint
    39  
    40  // TopicMessageQuery
    41  // Query that listens to messages sent to the specific TopicID
    42  type TopicMessageQuery struct {
    43  	errorHandler      func(stat status.Status)
    44  	completionHandler func()
    45  	retryHandler      func(err error) bool
    46  	attempt           uint64
    47  	maxAttempts       uint64
    48  	topicID           *TopicID
    49  	startTime         *time.Time
    50  	endTime           *time.Time
    51  	limit             uint64
    52  	mu                sync.Mutex
    53  }
    54  
    55  // NewTopicMessageQuery creates TopicMessageQuery which
    56  // listens to messages sent to the specific TopicID
    57  func NewTopicMessageQuery() *TopicMessageQuery {
    58  	return &TopicMessageQuery{
    59  		maxAttempts:       maxAttempts,
    60  		errorHandler:      _DefaultErrorHandler,
    61  		retryHandler:      _DefaultRetryHandler,
    62  		completionHandler: _DefaultCompletionHandler,
    63  	}
    64  }
    65  
    66  // SetTopicID Sets topic ID to retrieve messages for.
    67  // Required
    68  func (query *TopicMessageQuery) SetTopicID(topicID TopicID) *TopicMessageQuery {
    69  	query.topicID = &topicID
    70  	return query
    71  }
    72  
    73  // GetTopicID returns the TopicID for this TopicMessageQuery
    74  func (query *TopicMessageQuery) GetTopicID() TopicID {
    75  	if query.topicID == nil {
    76  		return TopicID{}
    77  	}
    78  
    79  	return *query.topicID
    80  }
    81  
    82  // SetStartTime Sets time for when to start listening for messages. Defaults to current time if
    83  // not set.
    84  func (query *TopicMessageQuery) SetStartTime(startTime time.Time) *TopicMessageQuery {
    85  	query.startTime = &startTime
    86  	return query
    87  }
    88  
    89  // GetStartTime returns the start time for this TopicMessageQuery
    90  func (query *TopicMessageQuery) GetStartTime() time.Time {
    91  	if query.startTime != nil {
    92  		return *query.startTime
    93  	}
    94  
    95  	return time.Time{}
    96  }
    97  
    98  // SetEndTime Sets time when to stop listening for messages. If not set it will receive
    99  // indefinitely.
   100  func (query *TopicMessageQuery) SetEndTime(endTime time.Time) *TopicMessageQuery {
   101  	query.endTime = &endTime
   102  	return query
   103  }
   104  
   105  func (query *TopicMessageQuery) GetEndTime() time.Time {
   106  	if query.endTime != nil {
   107  		return *query.endTime
   108  	}
   109  
   110  	return time.Time{}
   111  }
   112  
   113  // SetLimit Sets the maximum number of messages to receive before stopping. If not set or set to zero it will
   114  // return messages indefinitely.
   115  func (query *TopicMessageQuery) SetLimit(limit uint64) *TopicMessageQuery {
   116  	query.limit = limit
   117  	return query
   118  }
   119  
   120  func (query *TopicMessageQuery) GetLimit() uint64 {
   121  	return query.limit
   122  }
   123  
   124  // SetMaxAttempts Sets the amount of attempts to try to retrieve message
   125  func (query *TopicMessageQuery) SetMaxAttempts(maxAttempts uint64) *TopicMessageQuery {
   126  	query.maxAttempts = maxAttempts
   127  	return query
   128  }
   129  
   130  // GetMaxAttempts returns the amount of attempts to try to retrieve message
   131  func (query *TopicMessageQuery) GetMaxAttempts() uint64 {
   132  	return query.maxAttempts
   133  }
   134  
   135  // SetErrorHandler Sets the error handler for this query
   136  func (query *TopicMessageQuery) SetErrorHandler(errorHandler func(stat status.Status)) *TopicMessageQuery {
   137  	query.errorHandler = errorHandler
   138  	return query
   139  }
   140  
   141  // SetCompletionHandler Sets the completion handler for this query
   142  func (query *TopicMessageQuery) SetCompletionHandler(completionHandler func()) *TopicMessageQuery {
   143  	query.completionHandler = completionHandler
   144  	return query
   145  }
   146  
   147  // SetRetryHandler Sets the retry handler for this query
   148  func (query *TopicMessageQuery) SetRetryHandler(retryHandler func(err error) bool) *TopicMessageQuery {
   149  	query.retryHandler = retryHandler
   150  	return query
   151  }
   152  
   153  func (query *TopicMessageQuery) validateNetworkOnIDs(client *Client) error {
   154  	if client == nil || !client.autoValidateChecksums {
   155  		return nil
   156  	}
   157  
   158  	if query.topicID != nil {
   159  		if err := query.topicID.ValidateChecksum(client); err != nil {
   160  			return err
   161  		}
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  func (query *TopicMessageQuery) build() *mirror.ConsensusTopicQuery {
   168  	body := &mirror.ConsensusTopicQuery{
   169  		Limit: query.limit,
   170  	}
   171  	if query.topicID != nil {
   172  		body.TopicID = query.topicID._ToProtobuf()
   173  	}
   174  
   175  	if query.startTime != nil {
   176  		body.ConsensusStartTime = _TimeToProtobuf(*query.startTime)
   177  	} else {
   178  		body.ConsensusStartTime = &services.Timestamp{}
   179  	}
   180  
   181  	if query.endTime != nil {
   182  		body.ConsensusEndTime = _TimeToProtobuf(*query.endTime)
   183  	}
   184  
   185  	return body
   186  }
   187  
   188  // Subscribe subscribes to messages sent to the specific TopicID
   189  func (query *TopicMessageQuery) Subscribe(client *Client, onNext func(TopicMessage)) (SubscriptionHandle, error) {
   190  	var once sync.Once
   191  	done := make(chan struct{})
   192  	handle := SubscriptionHandle{}
   193  
   194  	err := query.validateNetworkOnIDs(client)
   195  	if err != nil {
   196  		return SubscriptionHandle{}, err
   197  	}
   198  
   199  	pb := query.build()
   200  
   201  	messages := make(map[string][]*mirror.ConsensusTopicResponse)
   202  
   203  	channel, err := client.mirrorNetwork._GetNextMirrorNode()._GetConsensusServiceClient()
   204  	if err != nil {
   205  		return handle, err
   206  	}
   207  
   208  	go func() {
   209  		query.mu.Lock()
   210  		defer query.mu.Unlock()
   211  		var subClient mirror.ConsensusService_SubscribeTopicClient
   212  		var err error
   213  
   214  		for {
   215  			if err != nil {
   216  				handle.Unsubscribe()
   217  
   218  				if grpcErr, ok := status.FromError(err); ok { // nolint
   219  					if query.attempt < query.maxAttempts && query.retryHandler(err) {
   220  						subClient = nil
   221  
   222  						delay := math.Min(250.0*math.Pow(2.0, float64(query.attempt)), 8000)
   223  						time.Sleep(time.Duration(delay) * time.Millisecond)
   224  						query.attempt++
   225  					} else {
   226  						query.errorHandler(*grpcErr)
   227  						break
   228  					}
   229  				} else if err == io.EOF {
   230  					query.completionHandler()
   231  					break
   232  				} else {
   233  					panic(err)
   234  				}
   235  			}
   236  
   237  			if subClient == nil {
   238  				ctx, cancel := context.WithCancel(context.TODO())
   239  				handle.onUnsubscribe = cancel
   240  				once.Do(func() {
   241  					close(done)
   242  				})
   243  				subClient, err = (*channel).SubscribeTopic(ctx, pb)
   244  
   245  				if err != nil {
   246  					continue
   247  				}
   248  			}
   249  
   250  			var resp *mirror.ConsensusTopicResponse
   251  			resp, err = subClient.Recv()
   252  
   253  			if err != nil {
   254  				continue
   255  			}
   256  
   257  			if resp.ConsensusTimestamp != nil {
   258  				pb.ConsensusStartTime = _TimeToProtobuf(_TimeFromProtobuf(resp.ConsensusTimestamp).Add(1 * time.Nanosecond))
   259  			}
   260  
   261  			if pb.Limit > 0 {
   262  				pb.Limit--
   263  			}
   264  
   265  			if resp.ChunkInfo == nil || resp.ChunkInfo.Total == 1 {
   266  				onNext(_TopicMessageOfSingle(resp))
   267  			} else {
   268  				txID := _TransactionIDFromProtobuf(resp.ChunkInfo.InitialTransactionID).String()
   269  				message, ok := messages[txID]
   270  				if !ok {
   271  					message = make([]*mirror.ConsensusTopicResponse, 0, resp.ChunkInfo.Total)
   272  				}
   273  
   274  				message = append(message, resp)
   275  				messages[txID] = message
   276  
   277  				if int32(len(message)) == resp.ChunkInfo.Total {
   278  					delete(messages, txID)
   279  
   280  					onNext(_TopicMessageOfMany(message))
   281  				}
   282  			}
   283  		}
   284  	}()
   285  	<-done
   286  	return handle, nil
   287  }
   288  
   289  func _DefaultErrorHandler(stat status.Status) {
   290  	println("Failed to subscribe to topic with status", stat.Code().String())
   291  }
   292  
   293  func _DefaultCompletionHandler() {
   294  	println("Subscription to topic finished")
   295  }
   296  
   297  func _DefaultRetryHandler(err error) bool {
   298  	code := status.Code(err)
   299  
   300  	switch code {
   301  	case codes.NotFound, codes.ResourceExhausted, codes.Unavailable:
   302  		return true
   303  	case codes.Internal:
   304  		grpcErr, ok := status.FromError(err)
   305  
   306  		if !ok {
   307  			return false
   308  		}
   309  
   310  		return rstStream.FindIndex([]byte(grpcErr.Message())) != nil
   311  	default:
   312  		return false
   313  	}
   314  }