main pico / pkg / pubsub / broker.go
Eric Bower  ·  2026-08-15
  1package pubsub
  2
  3import (
  4	"errors"
  5	"io"
  6	"iter"
  7	"log/slog"
  8	"path"
  9	"reflect"
 10	"strings"
 11	"sync"
 12	"time"
 13
 14	"github.com/antoniomika/syncmap"
 15)
 16
 17// HasWildcard checks if a topic string contains the wildcard character (*).
 18func HasWildcard(topic string) bool {
 19	return strings.Contains(topic, "*")
 20}
 21
 22// MatchTopic returns true if pattern matches topic exactly or via path.Match wildcarding.
 23func MatchTopic(pattern, topic string) bool {
 24	if pattern == topic {
 25		return true
 26	}
 27	if HasWildcard(pattern) {
 28		matched, err := path.Match(pattern, topic)
 29		return err == nil && matched
 30	}
 31	return false
 32}
 33
 34/*
 35Broker receives published messages and dispatches the message to the
 36subscribing clients. An message contains a message topic that clients
 37subscribe to and brokers use these subscription lists for determining the
 38clients to receive the message.
 39*/
 40type Broker interface {
 41	GetChannels() iter.Seq2[string, *Channel]
 42	GetClients() iter.Seq2[string, *Client]
 43	Connect(*Client, []*Channel) (error, error)
 44	SetDispatcher(dispatcher MessageDispatcher, channels []*Channel) error
 45}
 46
 47type BaseBroker struct {
 48	Channels *syncmap.Map[string, *Channel]
 49	Logger   *slog.Logger
 50}
 51
 52func (b *BaseBroker) Cleanup() {
 53	toRemove := []string{}
 54	for _, channel := range b.GetChannels() {
 55		count := 0
 56
 57		for range channel.GetClients() {
 58			count++
 59		}
 60
 61		if count == 0 {
 62			channel.Cleanup()
 63			toRemove = append(toRemove, channel.Topic)
 64		}
 65	}
 66
 67	for _, channel := range toRemove {
 68		b.Channels.Delete(channel)
 69	}
 70}
 71
 72func (b *BaseBroker) GetChannels() iter.Seq2[string, *Channel] {
 73	return b.Channels.Range
 74}
 75
 76func (b *BaseBroker) GetClients() iter.Seq2[string, *Client] {
 77	return func(yield func(string, *Client) bool) {
 78		for _, channel := range b.GetChannels() {
 79			channel.Clients.Range(yield)
 80		}
 81	}
 82}
 83
 84func (b *BaseBroker) Connect(client *Client, channels []*Channel) (error, error) {
 85	for _, channel := range channels {
 86		dataChannel := b.ensureChannel(channel)
 87		dataChannel.Clients.Store(client.ID, client)
 88		client.Channels.Store(dataChannel.Topic, dataChannel)
 89
 90		// If client is a subscriber and channel.Topic is a wildcard pattern,
 91		// attach client to all existing concrete channels matching the pattern.
 92		if (client.Direction == ChannelDirectionOutput || client.Direction == ChannelDirectionInputOutput) && HasWildcard(channel.Topic) {
 93			for _, existingChannel := range b.GetChannels() {
 94				if existingChannel.Topic != channel.Topic && !HasWildcard(existingChannel.Topic) && MatchTopic(channel.Topic, existingChannel.Topic) {
 95					existingChannel.Clients.Store(client.ID, client)
 96					client.Channels.Store(existingChannel.Topic, existingChannel)
 97				}
 98			}
 99		}
100
101		defer func() {
102			for _, ch := range client.GetChannels() {
103				ch.Clients.Delete(client.ID)
104				client.Channels.Delete(ch.Topic)
105			}
106			client.Channels.Delete(channel.Topic)
107			dataChannel.Clients.Delete(client.ID)
108
109			client.Cleanup()
110
111			inputCount := 0
112			pipeCount := 0
113			for _, cl := range dataChannel.GetClients() {
114				switch cl.Direction {
115				case ChannelDirectionInput:
116					inputCount++
117				case ChannelDirectionInputOutput:
118					pipeCount++
119				}
120			}
121
122			if inputCount == 0 && pipeCount <= 1 {
123				for _, cl := range dataChannel.GetClients() {
124					if !cl.KeepAlive {
125						otherChannels := 0
126						for _, ch := range cl.GetChannels() {
127							if ch.Topic != dataChannel.Topic {
128								otherChannels++
129							}
130						}
131						if otherChannels == 0 {
132							cl.Cleanup()
133						}
134					}
135				}
136			}
137
138			b.Cleanup()
139		}()
140	}
141
142	var (
143		inputErr  error
144		outputErr error
145		wg        sync.WaitGroup
146	)
147
148	// Pub
149	if client.Direction == ChannelDirectionInput || client.Direction == ChannelDirectionInputOutput {
150		wg.Add(1)
151		go func() {
152			defer wg.Done()
153			for {
154				data := make([]byte, 32*1024)
155				n, err := client.ReadWriter.Read(data)
156
157				data = data[:n]
158
159				channelMessage := ChannelMessage{
160					Data:      data,
161					ClientID:  client.ID,
162					Direction: ChannelDirectionInput,
163				}
164
165				if client.BlockWrite {
166				mainLoop:
167					for {
168						count := 0
169						for _, channel := range client.GetChannels() {
170							for _, chanClient := range channel.GetClients() {
171								if chanClient.Direction == ChannelDirectionOutput || chanClient.Direction == ChannelDirectionInputOutput {
172									count++
173								}
174							}
175						}
176
177						if count > 0 {
178							break mainLoop
179						}
180
181						select {
182						case <-client.Done:
183							break mainLoop
184						case <-time.After(1 * time.Millisecond):
185							continue
186						}
187					}
188				}
189
190				var sendwg sync.WaitGroup
191
192				for _, channel := range client.GetChannels() {
193					sendwg.Add(1)
194					go func() {
195						defer sendwg.Done()
196						select {
197						case channel.Data <- channelMessage:
198						case <-client.Done:
199						case <-channel.Done:
200						}
201					}()
202				}
203
204				sendwg.Wait()
205
206				if err != nil {
207					client.Cleanup()
208					if errors.Is(err, io.EOF) {
209						return
210					}
211					inputErr = err
212					return
213				}
214			}
215		}()
216	}
217
218	// Sub
219	if client.Direction == ChannelDirectionOutput || client.Direction == ChannelDirectionInputOutput {
220		wg.Add(1)
221		go func() {
222			defer wg.Done()
223		mainLoop:
224			for {
225				select {
226				case data, ok := <-client.Data:
227					_, err := client.ReadWriter.Write(data.Data)
228					if err != nil {
229						outputErr = err
230						client.Cleanup()
231						break mainLoop
232					}
233
234					if !ok {
235						break mainLoop
236					}
237				case <-client.Done:
238					break mainLoop
239				}
240			}
241		}()
242	}
243
244	done := make(chan struct{})
245	go func() {
246		wg.Wait()
247		close(done)
248	}()
249
250	select {
251	case <-done:
252	case <-client.Done:
253	}
254
255	return inputErr, outputErr
256}
257
258func (b *BaseBroker) ensureChannel(channel *Channel) *Channel {
259	dataChannel, loaded := b.Channels.LoadOrStore(channel.Topic, channel)
260	dataChannel.Handle()
261
262	// If this is a concrete (non-wildcard) channel created for the first time,
263	// attach any active wildcard subscribers whose pattern matches dataChannel.Topic.
264	if !loaded && !HasWildcard(channel.Topic) {
265		for _, existingChannel := range b.GetChannels() {
266			if HasWildcard(existingChannel.Topic) && MatchTopic(existingChannel.Topic, channel.Topic) {
267				for _, client := range existingChannel.GetClients() {
268					if client.Direction == ChannelDirectionOutput || client.Direction == ChannelDirectionInputOutput {
269						dataChannel.Clients.Store(client.ID, client)
270						client.Channels.Store(dataChannel.Topic, dataChannel)
271					}
272				}
273			}
274		}
275	}
276
277	return dataChannel
278}
279
280func (b *BaseBroker) SetDispatcher(dispatcher MessageDispatcher, channels []*Channel) error {
281	for _, channel := range channels {
282		dataChannel := b.ensureChannel(channel)
283		existingDispatcher := dataChannel.GetDispatcher()
284		if reflect.TypeOf(existingDispatcher) != reflect.TypeOf(dispatcher) {
285			dataChannel.SetDispatcher(dispatcher)
286		}
287	}
288	return nil
289}
290
291var _ Broker = (*BaseBroker)(nil)