github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/realtime/mem_realtime.go (about)

     1  package realtime
     2  
     3  import (
     4  	"sync"
     5  
     6  	"github.com/cozy/cozy-stack/pkg/logger"
     7  	"github.com/cozy/cozy-stack/pkg/prefixer"
     8  )
     9  
    10  var globalPrefixer = prefixer.NewPrefixer(prefixer.GlobalCouchCluster, "", "*")
    11  
    12  type memHub struct {
    13  	sync.RWMutex
    14  	topics        map[string]*topic
    15  	bySubscribers map[*Subscriber][]string // the list of topic keys by subscriber
    16  }
    17  
    18  func newMemHub() *memHub {
    19  	return &memHub{
    20  		topics:        make(map[string]*topic),
    21  		bySubscribers: make(map[*Subscriber][]string),
    22  	}
    23  }
    24  
    25  func (h *memHub) Publish(db prefixer.Prefixer, verb string, doc, oldDoc Doc) {
    26  	h.RLock()
    27  	defer h.RUnlock()
    28  
    29  	e := newEvent(db, verb, doc, oldDoc)
    30  	key := topicKey(db, doc.DocType())
    31  	it := h.topics[key]
    32  	if it != nil {
    33  		select {
    34  		case it.broadcast <- e:
    35  		case running := <-it.running:
    36  			logger.WithNamespace("realtime").
    37  				Warnf("unexpected state: publish with running=%v", running)
    38  			if !running {
    39  				delete(h.topics, key)
    40  			}
    41  		}
    42  	}
    43  	it = h.topics[topicKey(globalPrefixer, "*")]
    44  	if it != nil {
    45  		it.broadcast <- e
    46  	}
    47  }
    48  
    49  func (h *memHub) Subscriber(db prefixer.Prefixer) *Subscriber {
    50  	return newSubscriber(h, db)
    51  }
    52  
    53  func (h *memHub) SubscribeFirehose() *Subscriber {
    54  	sub := newSubscriber(h, globalPrefixer)
    55  	key := topicKey(sub, "*")
    56  	h.subscribe(sub, key)
    57  	return sub
    58  }
    59  
    60  func (h *memHub) subscribe(sub *Subscriber, key string) {
    61  	h.Lock()
    62  	go func() {
    63  		defer h.Unlock()
    64  
    65  		h.addTopic(sub, key)
    66  
    67  		w := &toWatch{sub, ""}
    68  		for {
    69  			it, exists := h.topics[key]
    70  			if !exists {
    71  				it = newTopic()
    72  				h.topics[key] = it
    73  			}
    74  
    75  			select {
    76  			case it.subscribe <- w:
    77  				return
    78  			case running := <-it.running:
    79  				logger.WithNamespace("realtime").
    80  					Warnf("unexpected state: subscribe with running=%v", running)
    81  				if !running {
    82  					delete(h.topics, key)
    83  				}
    84  			}
    85  		}
    86  	}()
    87  }
    88  
    89  func (h *memHub) unsubscribe(sub *Subscriber, key string) {
    90  	h.Lock()
    91  	go func() {
    92  		defer h.Unlock()
    93  
    94  		it, exists := h.topics[key]
    95  		if !exists {
    96  			return
    97  		}
    98  
    99  		h.removeTopic(sub, key)
   100  
   101  		w := &toWatch{sub, ""}
   102  		select {
   103  		case it.unsubscribe <- w:
   104  			if running := <-it.running; !running {
   105  				delete(h.topics, key)
   106  			}
   107  		case running := <-it.running:
   108  			logger.WithNamespace("realtime").
   109  				Warnf("unexpected state: unsubscribe with running=%v", running)
   110  			if !running {
   111  				delete(h.topics, key)
   112  			}
   113  		}
   114  	}()
   115  }
   116  
   117  func (h *memHub) watch(sub *Subscriber, key, id string) {
   118  	h.Lock()
   119  	go func() {
   120  		defer h.Unlock()
   121  
   122  		h.addTopic(sub, key)
   123  
   124  		w := &toWatch{sub, id}
   125  		for {
   126  			it, exists := h.topics[key]
   127  			if !exists {
   128  				it = newTopic()
   129  				h.topics[key] = it
   130  			}
   131  
   132  			select {
   133  			case it.subscribe <- w:
   134  				return
   135  			case running := <-it.running:
   136  				logger.WithNamespace("realtime").
   137  					Warnf("unexpected state: watch with running=%v", running)
   138  				if !running {
   139  					delete(h.topics, key)
   140  				}
   141  			}
   142  		}
   143  	}()
   144  }
   145  
   146  func (h *memHub) unwatch(sub *Subscriber, key, id string) {
   147  	h.Lock()
   148  	go func() {
   149  		defer h.Unlock()
   150  
   151  		it, exists := h.topics[key]
   152  		if !exists {
   153  			return
   154  		}
   155  
   156  		w := &toWatch{sub, id}
   157  		select {
   158  		case it.unsubscribe <- w:
   159  			if running := <-it.running; !running {
   160  				delete(h.topics, key)
   161  			}
   162  		case running := <-it.running:
   163  			logger.WithNamespace("realtime").
   164  				Warnf("unexpected state: unwatch with running=%v", running)
   165  			if !running {
   166  				delete(h.topics, key)
   167  			}
   168  		}
   169  	}()
   170  }
   171  
   172  func (h *memHub) close(sub *Subscriber) {
   173  	h.RLock()
   174  	list := h.bySubscribers[sub]
   175  	h.RUnlock()
   176  	keys := make([]string, len(list))
   177  	copy(keys, list)
   178  
   179  	for _, key := range keys {
   180  		h.unsubscribe(sub, key)
   181  	}
   182  }
   183  
   184  func (h *memHub) addTopic(sub *Subscriber, key string) {
   185  	list := h.bySubscribers[sub]
   186  	for _, k := range list {
   187  		if k == key {
   188  			return
   189  		}
   190  	}
   191  	list = append(list, key)
   192  	h.bySubscribers[sub] = list
   193  }
   194  
   195  func (h *memHub) removeTopic(sub *Subscriber, key string) {
   196  	list := h.bySubscribers[sub]
   197  	kept := list[:0]
   198  	for _, k := range list {
   199  		if k != key {
   200  			kept = append(kept, k)
   201  		}
   202  	}
   203  	if len(kept) == 0 {
   204  		delete(h.bySubscribers, sub)
   205  	} else {
   206  		h.bySubscribers[sub] = kept
   207  	}
   208  }
   209  
   210  func topicKey(db prefixer.Prefixer, doctype string) string {
   211  	return db.DBPrefix() + ":" + doctype
   212  }