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 }