github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/wsclient.go (about) 1 package rpcclient 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strconv" 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "github.com/gorilla/websocket" 14 "github.com/nspcc-dev/neo-go/pkg/core/block" 15 "github.com/nspcc-dev/neo-go/pkg/core/state" 16 "github.com/nspcc-dev/neo-go/pkg/core/transaction" 17 "github.com/nspcc-dev/neo-go/pkg/neorpc" 18 "github.com/nspcc-dev/neo-go/pkg/neorpc/result" 19 "github.com/nspcc-dev/neo-go/pkg/neorpc/rpcevent" 20 ) 21 22 // WSClient is a websocket-enabled RPC client that can be used with appropriate 23 // servers. It's supposed to be faster than Client because it has persistent 24 // connection to the server and at the same time it exposes some functionality 25 // that is only provided via websockets (like event subscription mechanism). 26 // WSClient is thread-safe and can be used from multiple goroutines to perform 27 // RPC requests. 28 // 29 // It exposes a set of Receive* methods with the same behaviour pattern that 30 // is caused by the fact that the client itself receives every message from the 31 // server via a single channel. This includes any subscriptions and any replies 32 // to ordinary requests at the same. The client then routes these messages to 33 // channels provided on subscription (passed to Receive*) or to the respective 34 // receivers (API callers) if it's an ordinary JSON-RPC reply. While synchronous 35 // API users are blocked during their calls and wake up on reply, subscription 36 // channels must be read from to avoid blocking the client. Failure to do so 37 // will make WSClient wait for the channel reader to get the event and while 38 // it waits every other messages (subscription-related or request replies) 39 // will be blocked. This also means that subscription channel must be properly 40 // drained after unsubscription. If CloseNotificationChannelIfFull option is on 41 // then the receiver channel will be closed immediately in case if a subsequent 42 // notification can't be sent to it, which means WSClient's operations are 43 // unblocking in this mode. No unsubscription is performed in this case, so it's 44 // still the user responsibility to unsubscribe. 45 // 46 // All Receive* methods provide notifications ordering and persistence guarantees. 47 // See https://github.com/nspcc-dev/neo-go/blob/master/docs/notifications.md#ordering-and-persistence-guarantees 48 // for more details on this topic. 49 // 50 // Any received subscription items (blocks/transactions/nofitications) are passed 51 // via pointers for efficiency, but the actual structures MUST NOT be changed, as 52 // it may affect the functionality of other notification receivers. If multiple 53 // subscriptions share the same receiver channel, then matching notification is 54 // only sent once per channel. The receiver channel will be closed by the WSClient 55 // immediately after MissedEvent is received from the server; no unsubscription 56 // is performed in this case, so it's the user responsibility to unsubscribe. It 57 // will also be closed on disconnection from server or on situation when it's 58 // impossible to send a subsequent notification to the subscriber's channel and 59 // CloseNotificationChannelIfFull option is on. 60 type WSClient struct { 61 Client 62 63 ws *websocket.Conn 64 wsOpts WSOptions 65 readerDone chan struct{} 66 writerDone chan struct{} 67 requests chan *neorpc.Request 68 shutdown chan struct{} 69 closeCalled atomic.Bool 70 71 closeErrLock sync.RWMutex 72 closeErr error 73 74 subscriptionsLock sync.RWMutex 75 subscriptions map[string]notificationReceiver 76 // receivers is a mapping from receiver channel to a set of corresponding subscription IDs. 77 // It must be accessed with subscriptionsLock taken. Its keys must be used to deliver 78 // notifications, if channel is not in the receivers list and corresponding subscription 79 // still exists, notification must not be sent. 80 receivers map[any][]string 81 82 respLock sync.RWMutex 83 respChannels map[uint64]chan *neorpc.Response 84 } 85 86 // WSOptions defines options for the web-socket RPC client. It contains a 87 // set of options for the underlying standard RPC client as far as 88 // WSClient-specific options. See Options documentation for more details. 89 type WSOptions struct { 90 Options 91 // CloseNotificationChannelIfFull allows WSClient to close a subscriber's 92 // receive channel in case if the channel isn't read properly and no more 93 // events can be pushed to it. This option, if set, allows to avoid WSClient 94 // blocking on a subsequent notification dispatch. However, if enabled, the 95 // corresponding subscription is kept even after receiver's channel closing, 96 // thus it's still the caller's duty to call Unsubscribe() for this 97 // subscription. 98 CloseNotificationChannelIfFull bool 99 } 100 101 // notificationReceiver is an interface aimed to provide WS subscriber functionality 102 // for different types of subscriptions. 103 type notificationReceiver interface { 104 // Comparator provides notification filtering functionality. 105 rpcevent.Comparator 106 // Receiver returns notification receiver channel. 107 Receiver() any 108 // TrySend checks whether notification passes receiver filter and sends it 109 // to the underlying channel if so. It is performed under subscriptions lock 110 // taken. nonBlocking denotes whether the receiving operation shouldn't block 111 // the client's operation. It returns whether notification matches the filter 112 // and whether the receiver channel is overflown. 113 TrySend(ntf Notification, nonBlocking bool) (bool, bool) 114 // Close closes underlying receiver channel. 115 Close() 116 } 117 118 // blockReceiver stores information about block events subscriber. 119 type blockReceiver struct { 120 filter *neorpc.BlockFilter 121 ch chan<- *block.Block 122 } 123 124 // EventID implements neorpc.Comparator interface. 125 func (r *blockReceiver) EventID() neorpc.EventID { 126 return neorpc.BlockEventID 127 } 128 129 // Filter implements neorpc.Comparator interface. 130 func (r *blockReceiver) Filter() neorpc.SubscriptionFilter { 131 if r.filter == nil { 132 return nil 133 } 134 return *r.filter 135 } 136 137 // Receiver implements notificationReceiver interface. 138 func (r *blockReceiver) Receiver() any { 139 return r.ch 140 } 141 142 // TrySend implements notificationReceiver interface. 143 func (r *blockReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 144 if rpcevent.Matches(r, ntf) { 145 if nonBlocking { 146 select { 147 case r.ch <- ntf.Value.(*block.Block): 148 default: 149 return true, true 150 } 151 } else { 152 r.ch <- ntf.Value.(*block.Block) 153 } 154 155 return true, false 156 } 157 return false, false 158 } 159 160 // Close implements notificationReceiver interface. 161 func (r *blockReceiver) Close() { 162 close(r.ch) 163 } 164 165 // headerOfAddedBlockReceiver stores information about header of added block events subscriber. 166 type headerOfAddedBlockReceiver struct { 167 filter *neorpc.BlockFilter 168 ch chan<- *block.Header 169 } 170 171 // EventID implements neorpc.Comparator interface. 172 func (r *headerOfAddedBlockReceiver) EventID() neorpc.EventID { 173 return neorpc.HeaderOfAddedBlockEventID 174 } 175 176 // Filter implements neorpc.Comparator interface. 177 func (r *headerOfAddedBlockReceiver) Filter() neorpc.SubscriptionFilter { 178 if r.filter == nil { 179 return nil 180 } 181 return *r.filter 182 } 183 184 // Receiver implements notificationReceiver interface. 185 func (r *headerOfAddedBlockReceiver) Receiver() any { 186 return r.ch 187 } 188 189 // TrySend implements notificationReceiver interface. 190 func (r *headerOfAddedBlockReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 191 if rpcevent.Matches(r, ntf) { 192 if nonBlocking { 193 select { 194 case r.ch <- ntf.Value.(*block.Header): 195 default: 196 return true, true 197 } 198 } else { 199 r.ch <- ntf.Value.(*block.Header) 200 } 201 return true, false 202 } 203 return false, false 204 } 205 206 // Close implements notificationReceiver interface. 207 func (r *headerOfAddedBlockReceiver) Close() { 208 close(r.ch) 209 } 210 211 // txReceiver stores information about transaction events subscriber. 212 type txReceiver struct { 213 filter *neorpc.TxFilter 214 ch chan<- *transaction.Transaction 215 } 216 217 // EventID implements neorpc.Comparator interface. 218 func (r *txReceiver) EventID() neorpc.EventID { 219 return neorpc.TransactionEventID 220 } 221 222 // Filter implements neorpc.Comparator interface. 223 func (r *txReceiver) Filter() neorpc.SubscriptionFilter { 224 if r.filter == nil { 225 return nil 226 } 227 return *r.filter 228 } 229 230 // Receiver implements notificationReceiver interface. 231 func (r *txReceiver) Receiver() any { 232 return r.ch 233 } 234 235 // TrySend implements notificationReceiver interface. 236 func (r *txReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 237 if rpcevent.Matches(r, ntf) { 238 if nonBlocking { 239 select { 240 case r.ch <- ntf.Value.(*transaction.Transaction): 241 default: 242 return true, true 243 } 244 } else { 245 r.ch <- ntf.Value.(*transaction.Transaction) 246 } 247 248 return true, false 249 } 250 return false, false 251 } 252 253 // Close implements notificationReceiver interface. 254 func (r *txReceiver) Close() { 255 close(r.ch) 256 } 257 258 // executionNotificationReceiver stores information about execution notifications subscriber. 259 type executionNotificationReceiver struct { 260 filter *neorpc.NotificationFilter 261 ch chan<- *state.ContainedNotificationEvent 262 } 263 264 // EventID implements neorpc.Comparator interface. 265 func (r *executionNotificationReceiver) EventID() neorpc.EventID { 266 return neorpc.NotificationEventID 267 } 268 269 // Filter implements neorpc.Comparator interface. 270 func (r *executionNotificationReceiver) Filter() neorpc.SubscriptionFilter { 271 if r.filter == nil { 272 return nil 273 } 274 return *r.filter 275 } 276 277 // Receiver implements notificationReceiver interface. 278 func (r *executionNotificationReceiver) Receiver() any { 279 return r.ch 280 } 281 282 // TrySend implements notificationReceiver interface. 283 func (r *executionNotificationReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 284 if rpcevent.Matches(r, ntf) { 285 if nonBlocking { 286 select { 287 case r.ch <- ntf.Value.(*state.ContainedNotificationEvent): 288 default: 289 return true, true 290 } 291 } else { 292 r.ch <- ntf.Value.(*state.ContainedNotificationEvent) 293 } 294 295 return true, false 296 } 297 return false, false 298 } 299 300 // Close implements notificationReceiver interface. 301 func (r *executionNotificationReceiver) Close() { 302 close(r.ch) 303 } 304 305 // executionReceiver stores information about application execution results subscriber. 306 type executionReceiver struct { 307 filter *neorpc.ExecutionFilter 308 ch chan<- *state.AppExecResult 309 } 310 311 // EventID implements neorpc.Comparator interface. 312 func (r *executionReceiver) EventID() neorpc.EventID { 313 return neorpc.ExecutionEventID 314 } 315 316 // Filter implements neorpc.Comparator interface. 317 func (r *executionReceiver) Filter() neorpc.SubscriptionFilter { 318 if r.filter == nil { 319 return nil 320 } 321 return *r.filter 322 } 323 324 // Receiver implements notificationReceiver interface. 325 func (r *executionReceiver) Receiver() any { 326 return r.ch 327 } 328 329 // TrySend implements notificationReceiver interface. 330 func (r *executionReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 331 if rpcevent.Matches(r, ntf) { 332 if nonBlocking { 333 select { 334 case r.ch <- ntf.Value.(*state.AppExecResult): 335 default: 336 return true, true 337 } 338 } else { 339 r.ch <- ntf.Value.(*state.AppExecResult) 340 } 341 342 return true, false 343 } 344 return false, false 345 } 346 347 // Close implements notificationReceiver interface. 348 func (r *executionReceiver) Close() { 349 close(r.ch) 350 } 351 352 // notaryRequestReceiver stores information about notary requests subscriber. 353 type notaryRequestReceiver struct { 354 filter *neorpc.NotaryRequestFilter 355 ch chan<- *result.NotaryRequestEvent 356 } 357 358 // EventID implements neorpc.Comparator interface. 359 func (r *notaryRequestReceiver) EventID() neorpc.EventID { 360 return neorpc.NotaryRequestEventID 361 } 362 363 // Filter implements neorpc.Comparator interface. 364 func (r *notaryRequestReceiver) Filter() neorpc.SubscriptionFilter { 365 if r.filter == nil { 366 return nil 367 } 368 return *r.filter 369 } 370 371 // Receiver implements notificationReceiver interface. 372 func (r *notaryRequestReceiver) Receiver() any { 373 return r.ch 374 } 375 376 // TrySend implements notificationReceiver interface. 377 func (r *notaryRequestReceiver) TrySend(ntf Notification, nonBlocking bool) (bool, bool) { 378 if rpcevent.Matches(r, ntf) { 379 if nonBlocking { 380 select { 381 case r.ch <- ntf.Value.(*result.NotaryRequestEvent): 382 default: 383 return true, true 384 } 385 } else { 386 r.ch <- ntf.Value.(*result.NotaryRequestEvent) 387 } 388 389 return true, false 390 } 391 return false, false 392 } 393 394 // Close implements notificationReceiver interface. 395 func (r *notaryRequestReceiver) Close() { 396 close(r.ch) 397 } 398 399 // Notification represents a server-generated notification for client subscriptions. 400 // Value can be one of *block.Block, *state.AppExecResult, *state.ContainedNotificationEvent 401 // *transaction.Transaction or *subscriptions.NotaryRequestEvent based on Type. 402 type Notification struct { 403 Type neorpc.EventID 404 Value any 405 } 406 407 // EventID implements Container interface and returns notification ID. 408 func (n Notification) EventID() neorpc.EventID { 409 return n.Type 410 } 411 412 // EventPayload implements Container interface and returns notification 413 // object. 414 func (n Notification) EventPayload() any { 415 return n.Value 416 } 417 418 // requestResponse is a combined type for request and response since we can get 419 // any of them here. 420 type requestResponse struct { 421 neorpc.Response 422 Method string `json:"method"` 423 RawParams []json.RawMessage `json:"params,omitempty"` 424 } 425 426 const ( 427 // Message limit for receiving side. 428 wsReadLimit = 10 * 1024 * 1024 429 430 // Disconnection timeout. 431 wsPongLimit = 60 * time.Second 432 433 // Ping period for connection liveness check. 434 wsPingPeriod = wsPongLimit / 2 435 436 // Write deadline. 437 wsWriteLimit = wsPingPeriod / 2 438 ) 439 440 // ErrNilNotificationReceiver is returned when notification receiver channel is nil. 441 var ErrNilNotificationReceiver = errors.New("nil notification receiver") 442 443 // ErrWSConnLost is a WSClient-specific error that will be returned for any 444 // requests after disconnection (including intentional ones via 445 // (*WSClient).Close). 446 var ErrWSConnLost = errors.New("connection lost") 447 448 // errConnClosedByUser is a WSClient error used iff the user calls (*WSClient).Close method by himself. 449 var errConnClosedByUser = errors.New("connection closed by user") 450 451 // NewWS returns a new WSClient ready to use (with established websocket 452 // connection). You need to use websocket URL for it like `ws://1.2.3.4/ws`. 453 // You should call Init method to initialize the network magic the client is 454 // operating on. 455 func NewWS(ctx context.Context, endpoint string, opts WSOptions) (*WSClient, error) { 456 dialer := websocket.Dialer{HandshakeTimeout: opts.DialTimeout} 457 ws, resp, err := dialer.DialContext(ctx, endpoint, nil) 458 if resp != nil && resp.Body != nil { // Can be non-nil even with error returned. 459 defer resp.Body.Close() // Not exactly required by websocket, but let's do this for bodyclose checker. 460 } 461 if err != nil { 462 if resp != nil && resp.Body != nil { 463 var srvErr neorpc.HeaderAndError 464 465 dec := json.NewDecoder(resp.Body) 466 decErr := dec.Decode(&srvErr) 467 if decErr == nil && srvErr.Error != nil { 468 err = srvErr.Error 469 } 470 } 471 return nil, err 472 } 473 wsc := &WSClient{ 474 Client: Client{}, 475 476 ws: ws, 477 wsOpts: opts, 478 shutdown: make(chan struct{}), 479 readerDone: make(chan struct{}), 480 writerDone: make(chan struct{}), 481 respChannels: make(map[uint64]chan *neorpc.Response), 482 requests: make(chan *neorpc.Request), 483 subscriptions: make(map[string]notificationReceiver), 484 receivers: make(map[any][]string), 485 } 486 487 err = initClient(ctx, &wsc.Client, endpoint, opts.Options) 488 if err != nil { 489 return nil, err 490 } 491 wsc.Client.cli = nil 492 493 go wsc.wsReader() 494 go wsc.wsWriter() 495 wsc.requestF = wsc.makeWsRequest 496 return wsc, nil 497 } 498 499 // Close closes connection to the remote side rendering this client instance 500 // unusable. 501 func (c *WSClient) Close() { 502 if c.closeCalled.CompareAndSwap(false, true) { 503 c.setCloseErr(errConnClosedByUser) 504 // Closing shutdown channel sends a signal to wsWriter to break out of the 505 // loop. In doing so it does ws.Close() closing the network connection 506 // which in turn makes wsReader receive an err from ws.ReadJSON() and also 507 // break out of the loop closing c.done channel in its shutdown sequence. 508 close(c.shutdown) 509 // Call to cancel will send signal to all users of Context(). 510 c.Client.ctxCancel() 511 } 512 <-c.readerDone 513 } 514 515 func (c *WSClient) wsReader() { 516 c.ws.SetReadLimit(wsReadLimit) 517 c.ws.SetPongHandler(func(string) error { 518 err := c.ws.SetReadDeadline(time.Now().Add(wsPongLimit)) 519 if err != nil { 520 c.setCloseErr(fmt.Errorf("failed to set pong read deadline: %w", err)) 521 } 522 return err 523 }) 524 var connCloseErr error 525 readloop: 526 for { 527 rr := new(requestResponse) 528 err := c.ws.SetReadDeadline(time.Now().Add(wsPongLimit)) 529 if err != nil { 530 connCloseErr = fmt.Errorf("failed to set response read deadline: %w", err) 531 break readloop 532 } 533 err = c.ws.ReadJSON(rr) 534 if err != nil { 535 // Timeout/connection loss/malformed response. 536 connCloseErr = fmt.Errorf("failed to read JSON response (timeout/connection loss/malformed response): %w", err) 537 break readloop 538 } 539 if rr.ID == nil && rr.Method != "" { 540 event, err := neorpc.GetEventIDFromString(rr.Method) 541 if err != nil { 542 // Bad event received. 543 connCloseErr = fmt.Errorf("failed to perse event ID from string %s: %w", rr.Method, err) 544 break readloop 545 } 546 if event != neorpc.MissedEventID && len(rr.RawParams) != 1 { 547 // Bad event received. 548 connCloseErr = fmt.Errorf("bad event received: %s / %d", event, len(rr.RawParams)) 549 break readloop 550 } 551 ntf := Notification{Type: event} 552 switch event { 553 case neorpc.BlockEventID: 554 sr, err := c.stateRootInHeader() 555 if err != nil { 556 // Client is not initialized. 557 connCloseErr = fmt.Errorf("failed to fetch StateRootInHeader: %w", err) 558 break readloop 559 } 560 ntf.Value = block.New(sr) 561 case neorpc.TransactionEventID: 562 ntf.Value = &transaction.Transaction{} 563 case neorpc.NotificationEventID: 564 ntf.Value = new(state.ContainedNotificationEvent) 565 case neorpc.ExecutionEventID: 566 ntf.Value = new(state.AppExecResult) 567 case neorpc.NotaryRequestEventID: 568 ntf.Value = new(result.NotaryRequestEvent) 569 case neorpc.HeaderOfAddedBlockEventID: 570 sr, err := c.stateRootInHeader() 571 if err != nil { 572 // Client is not initialized. 573 connCloseErr = fmt.Errorf("failed to fetch StateRootInHeader: %w", err) 574 break readloop 575 } 576 ntf.Value = &block.New(sr).Header 577 case neorpc.MissedEventID: 578 // No value. 579 default: 580 // Bad event received. 581 connCloseErr = fmt.Errorf("unknown event received: %d", event) 582 break readloop 583 } 584 if event != neorpc.MissedEventID { 585 err = json.Unmarshal(rr.RawParams[0], ntf.Value) 586 if err != nil { 587 // Bad event received. 588 connCloseErr = fmt.Errorf("failed to unmarshal event of type %s from JSON: %w", event, err) 589 break readloop 590 } 591 } 592 c.notifySubscribers(ntf) 593 } else if rr.ID != nil && (rr.Error != nil || rr.Result != nil) { 594 id, err := strconv.ParseUint(string(rr.ID), 10, 64) 595 if err != nil { 596 connCloseErr = fmt.Errorf("failed to retrieve response ID from string %s: %w", string(rr.ID), err) 597 break readloop // Malformed response (invalid response ID). 598 } 599 ch := c.getResponseChannel(id) 600 if ch == nil { 601 connCloseErr = fmt.Errorf("unknown response channel for response %d", id) 602 break readloop // Unknown response (unexpected response ID). 603 } 604 ch <- &rr.Response 605 } else { 606 // Malformed response, neither valid request, nor valid response. 607 connCloseErr = fmt.Errorf("malformed response") 608 break readloop 609 } 610 } 611 if connCloseErr != nil { 612 c.setCloseErr(connCloseErr) 613 } 614 close(c.readerDone) 615 c.respLock.Lock() 616 for _, ch := range c.respChannels { 617 close(ch) 618 } 619 c.respChannels = nil 620 c.respLock.Unlock() 621 c.subscriptionsLock.Lock() 622 for rcvrCh, ids := range c.receivers { 623 c.dropSubCh(rcvrCh, ids[0], true) 624 } 625 c.subscriptionsLock.Unlock() 626 c.Client.ctxCancel() 627 } 628 629 // dropSubCh closes corresponding subscriber's channel and removes it from the 630 // receivers map. The channel is still being kept in 631 // the subscribers map as technically the server-side subscription still exists 632 // and the user is responsible for unsubscription. This method must be called 633 // under subscriptionsLock taken. It's the caller's duty to ensure dropSubCh 634 // will be called once per channel, otherwise panic will occur. 635 func (c *WSClient) dropSubCh(rcvrCh any, id string, ignoreCloseNotificationChannelIfFull bool) { 636 if ignoreCloseNotificationChannelIfFull || c.wsOpts.CloseNotificationChannelIfFull { 637 c.subscriptions[id].Close() 638 delete(c.receivers, rcvrCh) 639 } 640 } 641 642 func (c *WSClient) wsWriter() { 643 pingTicker := time.NewTicker(wsPingPeriod) 644 defer c.ws.Close() 645 defer pingTicker.Stop() 646 defer close(c.writerDone) 647 var connCloseErr error 648 writeloop: 649 for { 650 select { 651 case <-c.shutdown: 652 return 653 case <-c.readerDone: 654 return 655 case req, ok := <-c.requests: 656 if !ok { 657 return 658 } 659 if err := c.ws.SetWriteDeadline(time.Now().Add(c.opts.RequestTimeout)); err != nil { 660 connCloseErr = fmt.Errorf("failed to set request write deadline: %w", err) 661 break writeloop 662 } 663 if err := c.ws.WriteJSON(req); err != nil { 664 connCloseErr = fmt.Errorf("failed to write JSON request (%s / %d): %w", req.Method, len(req.Params), err) 665 break writeloop 666 } 667 case <-pingTicker.C: 668 if err := c.ws.SetWriteDeadline(time.Now().Add(wsWriteLimit)); err != nil { 669 connCloseErr = fmt.Errorf("failed to set ping write deadline: %w", err) 670 break writeloop 671 } 672 if err := c.ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { 673 connCloseErr = fmt.Errorf("failed to write ping message: %w", err) 674 break writeloop 675 } 676 } 677 } 678 if connCloseErr != nil { 679 c.setCloseErr(connCloseErr) 680 } 681 } 682 683 func (c *WSClient) notifySubscribers(ntf Notification) { 684 if ntf.Type == neorpc.MissedEventID { 685 c.subscriptionsLock.Lock() 686 for rcvr, ids := range c.receivers { 687 c.subscriptions[ids[0]].Close() 688 delete(c.receivers, rcvr) 689 } 690 c.subscriptionsLock.Unlock() 691 return 692 } 693 c.subscriptionsLock.Lock() 694 for rcvrCh, ids := range c.receivers { 695 for _, id := range ids { 696 ok, dropCh := c.subscriptions[id].TrySend(ntf, c.wsOpts.CloseNotificationChannelIfFull) 697 if dropCh { 698 c.dropSubCh(rcvrCh, id, false) 699 break // strictly single drop per channel 700 } 701 if ok { 702 break // strictly one notification per channel 703 } 704 } 705 } 706 c.subscriptionsLock.Unlock() 707 } 708 709 func (c *WSClient) unregisterRespChannel(id uint64) { 710 c.respLock.Lock() 711 defer c.respLock.Unlock() 712 if ch, ok := c.respChannels[id]; ok { 713 delete(c.respChannels, id) 714 close(ch) 715 } 716 } 717 718 func (c *WSClient) getResponseChannel(id uint64) chan *neorpc.Response { 719 c.respLock.RLock() 720 defer c.respLock.RUnlock() 721 return c.respChannels[id] 722 } 723 724 // closeErrOrConnLost returns the error that may occur either in wsReader or wsWriter. 725 // If wsReader or wsWriter do not set the error, it returns ErrWSConnLost. 726 func (c *WSClient) closeErrOrConnLost() (err error) { 727 err = ErrWSConnLost 728 if closeErr := c.GetError(); closeErr != nil { 729 err = closeErr 730 } 731 return 732 } 733 734 func (c *WSClient) makeWsRequest(r *neorpc.Request) (*neorpc.Response, error) { 735 ch := make(chan *neorpc.Response) 736 c.respLock.Lock() 737 select { 738 case <-c.readerDone: 739 c.respLock.Unlock() 740 return nil, fmt.Errorf("%w: before registering response channel", c.closeErrOrConnLost()) 741 default: 742 c.respChannels[r.ID] = ch 743 c.respLock.Unlock() 744 } 745 select { 746 case <-c.readerDone: 747 return nil, fmt.Errorf("%w: before sending the request", c.closeErrOrConnLost()) 748 case <-c.writerDone: 749 return nil, fmt.Errorf("%w: before sending the request", c.closeErrOrConnLost()) 750 case c.requests <- r: 751 } 752 select { 753 case <-c.readerDone: 754 return nil, fmt.Errorf("%w: while waiting for the response", c.closeErrOrConnLost()) 755 case <-c.writerDone: 756 return nil, fmt.Errorf("%w: while waiting for the response", c.closeErrOrConnLost()) 757 case resp, ok := <-ch: 758 if !ok { 759 return nil, fmt.Errorf("%w: while waiting for the response", c.closeErrOrConnLost()) 760 } 761 c.unregisterRespChannel(r.ID) 762 return resp, nil 763 } 764 } 765 766 func (c *WSClient) performSubscription(params []any, rcvr notificationReceiver) (string, error) { 767 var resp string 768 769 if flt := rcvr.Filter(); flt != nil { 770 if err := flt.IsValid(); err != nil { 771 return "", err 772 } 773 } 774 if err := c.performRequest("subscribe", params, &resp); err != nil { 775 return "", err 776 } 777 778 c.subscriptionsLock.Lock() 779 defer c.subscriptionsLock.Unlock() 780 781 c.subscriptions[resp] = rcvr 782 ch := rcvr.Receiver() 783 c.receivers[ch] = append(c.receivers[ch], resp) 784 return resp, nil 785 } 786 787 // ReceiveBlocks registers provided channel as a receiver for the new block events. 788 // Events can be filtered by the given BlockFilter, nil value doesn't add any filter. 789 // See WSClient comments for generic Receive* behaviour details. 790 func (c *WSClient) ReceiveBlocks(flt *neorpc.BlockFilter, rcvr chan<- *block.Block) (string, error) { 791 if rcvr == nil { 792 return "", ErrNilNotificationReceiver 793 } 794 if !c.cache.initDone { 795 return "", errNetworkNotInitialized 796 } 797 params := []any{"block_added"} 798 if flt != nil { 799 flt = flt.Copy() 800 params = append(params, *flt) 801 } 802 r := &blockReceiver{ 803 filter: flt, 804 ch: rcvr, 805 } 806 return c.performSubscription(params, r) 807 } 808 809 // ReceiveHeadersOfAddedBlocks registers provided channel as a receiver for new 810 // block's header events. Events can be filtered by the given [neorpc.BlockFilter], 811 // nil value doesn't add any filter. See WSClient comments for generic 812 // Receive* behaviour details. 813 func (c *WSClient) ReceiveHeadersOfAddedBlocks(flt *neorpc.BlockFilter, rcvr chan<- *block.Header) (string, error) { 814 if rcvr == nil { 815 return "", ErrNilNotificationReceiver 816 } 817 if !c.cache.initDone { 818 return "", errNetworkNotInitialized 819 } 820 params := []any{"header_of_added_block"} 821 if flt != nil { 822 flt = flt.Copy() 823 params = append(params, *flt) 824 } 825 r := &headerOfAddedBlockReceiver{ 826 filter: flt, 827 ch: rcvr, 828 } 829 return c.performSubscription(params, r) 830 } 831 832 // ReceiveTransactions registers provided channel as a receiver for new transaction 833 // events. Events can be filtered by the given TxFilter, nil value doesn't add any 834 // filter. See WSClient comments for generic Receive* behaviour details. 835 func (c *WSClient) ReceiveTransactions(flt *neorpc.TxFilter, rcvr chan<- *transaction.Transaction) (string, error) { 836 if rcvr == nil { 837 return "", ErrNilNotificationReceiver 838 } 839 params := []any{"transaction_added"} 840 if flt != nil { 841 flt = flt.Copy() 842 params = append(params, *flt) 843 } 844 r := &txReceiver{ 845 filter: flt, 846 ch: rcvr, 847 } 848 return c.performSubscription(params, r) 849 } 850 851 // ReceiveExecutionNotifications registers provided channel as a receiver for execution 852 // events. Events can be filtered by the given NotificationFilter, nil value doesn't add 853 // any filter. See WSClient comments for generic Receive* behaviour details. 854 func (c *WSClient) ReceiveExecutionNotifications(flt *neorpc.NotificationFilter, rcvr chan<- *state.ContainedNotificationEvent) (string, error) { 855 if rcvr == nil { 856 return "", ErrNilNotificationReceiver 857 } 858 params := []any{"notification_from_execution"} 859 if flt != nil { 860 flt = flt.Copy() 861 params = append(params, *flt) 862 } 863 r := &executionNotificationReceiver{ 864 filter: flt, 865 ch: rcvr, 866 } 867 return c.performSubscription(params, r) 868 } 869 870 // ReceiveExecutions registers provided channel as a receiver for 871 // application execution result events generated during transaction execution. 872 // Events can be filtered by the given ExecutionFilter, nil value doesn't add any filter. 873 // See WSClient comments for generic Receive* behaviour details. 874 func (c *WSClient) ReceiveExecutions(flt *neorpc.ExecutionFilter, rcvr chan<- *state.AppExecResult) (string, error) { 875 if rcvr == nil { 876 return "", ErrNilNotificationReceiver 877 } 878 params := []any{"transaction_executed"} 879 if flt != nil { 880 flt = flt.Copy() 881 params = append(params, *flt) 882 } 883 r := &executionReceiver{ 884 filter: flt, 885 ch: rcvr, 886 } 887 return c.performSubscription(params, r) 888 } 889 890 // ReceiveNotaryRequests registers provided channel as a receiver for notary request 891 // payload addition or removal events. Events can be filtered by the given NotaryRequestFilter 892 // where sender corresponds to notary request sender (the second fallback transaction 893 // signer), signer corresponds to main transaction signers and type corresponds to the 894 // [mempoolevent.Type] and denotes whether notary request was added to or removed from 895 // the notary request pool. nil value doesn't add any filter. See WSClient comments 896 // for generic Receive* behaviour details. 897 func (c *WSClient) ReceiveNotaryRequests(flt *neorpc.NotaryRequestFilter, rcvr chan<- *result.NotaryRequestEvent) (string, error) { 898 if rcvr == nil { 899 return "", ErrNilNotificationReceiver 900 } 901 params := []any{"notary_request_event"} 902 if flt != nil { 903 flt = flt.Copy() 904 params = append(params, *flt) 905 } 906 r := ¬aryRequestReceiver{ 907 filter: flt, 908 ch: rcvr, 909 } 910 return c.performSubscription(params, r) 911 } 912 913 // Unsubscribe removes subscription for the given event stream. It will return an 914 // error in case if there's no subscription with the provided ID. Call to Unsubscribe 915 // doesn't block notifications receive process for given subscriber, thus, ensure 916 // that subscriber channel is properly drained while unsubscription is being 917 // performed. Failing to do so will cause WSClient to block even regular requests. 918 // You may probably need to run unsubscription process in a separate 919 // routine (in parallel with notification receiver routine) to avoid Client's 920 // notification dispatcher blocking. 921 func (c *WSClient) Unsubscribe(id string) error { 922 return c.performUnsubscription(id) 923 } 924 925 // UnsubscribeAll removes all active subscriptions of the current client. It copies 926 // the list of subscribers in order not to hold the lock for the whole execution 927 // time and tries to unsubscribe from us many feeds as possible returning the 928 // chunk of unsubscription errors afterwards. Call to UnsubscribeAll doesn't block 929 // notifications receive process for given subscribers, thus, ensure that subscribers 930 // channels are properly drained while unsubscription is being performed. Failing to 931 // do so will cause WSClient to block even regular requests. You may probably need 932 // to run unsubscription process in a separate routine (in parallel with notification 933 // receiver routines) to avoid Client's notification dispatcher blocking. 934 func (c *WSClient) UnsubscribeAll() error { 935 c.subscriptionsLock.Lock() 936 subs := make([]string, 0, len(c.subscriptions)) 937 for id := range c.subscriptions { 938 subs = append(subs, id) 939 } 940 c.subscriptionsLock.Unlock() 941 942 var resErr error 943 for _, id := range subs { 944 err := c.performUnsubscription(id) 945 if err != nil { 946 errFmt := "failed to unsubscribe from feed %d: %w" 947 errArgs := []any{err} 948 if resErr != nil { 949 errFmt = "%w; " + errFmt 950 errArgs = append([]any{resErr}, errArgs...) 951 } 952 resErr = fmt.Errorf(errFmt, errArgs...) 953 } 954 } 955 return resErr 956 } 957 958 // performUnsubscription is internal method that removes subscription with the given 959 // ID from the list of subscriptions and receivers. It takes the subscriptions lock 960 // after WS RPC unsubscription request is completed. Until then the subscriber channel 961 // may still receive WS notifications. 962 func (c *WSClient) performUnsubscription(id string) error { 963 var resp bool 964 if err := c.performRequest("unsubscribe", []any{id}, &resp); err != nil { 965 return err 966 } 967 if !resp { 968 return errors.New("unsubscribe method returned false result") 969 } 970 971 c.subscriptionsLock.Lock() 972 defer c.subscriptionsLock.Unlock() 973 974 rcvr, ok := c.subscriptions[id] 975 if !ok { 976 return errors.New("no subscription with this ID") 977 } 978 ch := rcvr.Receiver() 979 ids := c.receivers[ch] 980 for i, rcvrID := range ids { 981 if rcvrID == id { 982 ids = append(ids[:i], ids[i+1:]...) 983 break 984 } 985 } 986 if len(ids) == 0 { 987 delete(c.receivers, ch) 988 } else { 989 c.receivers[ch] = ids 990 } 991 delete(c.subscriptions, id) 992 return nil 993 } 994 995 // setCloseErr is a thread-safe method setting closeErr in case if it's not yet set. 996 func (c *WSClient) setCloseErr(err error) { 997 c.closeErrLock.Lock() 998 defer c.closeErrLock.Unlock() 999 1000 if c.closeErr == nil { 1001 c.closeErr = err 1002 } 1003 } 1004 1005 // GetError returns the reason of WS connection closing. It returns nil in case if connection 1006 // was closed by the use via Close() method calling. 1007 func (c *WSClient) GetError() error { 1008 c.closeErrLock.RLock() 1009 defer c.closeErrLock.RUnlock() 1010 1011 if c.closeErr != nil && errors.Is(c.closeErr, errConnClosedByUser) { 1012 return nil 1013 } 1014 return c.closeErr 1015 } 1016 1017 // Context returns WSClient Cancel context that will be terminated on Client shutdown. 1018 func (c *WSClient) Context() context.Context { 1019 return c.Client.ctx 1020 }