88 lines
3.0 KiB
Go
88 lines
3.0 KiB
Go
// Package notify is a minimal in-process pub-sub for "a message landed in this
|
|
// mailbox folder" events — the single relay point SMTP local delivery and IMAP APPEND
|
|
// call into, and both IMAP IDLE push (internal/imapserver) and webmail SSE push
|
|
// (internal/webui) subscribe to. Deliberately just a wakeup signal with no message
|
|
// content: every subscriber already has its own authoritative way to re-fetch state
|
|
// (a DB query), so there's no duplicate-source-of-truth risk in keeping the event
|
|
// itself empty.
|
|
package notify
|
|
|
|
import "sync"
|
|
|
|
type key struct {
|
|
mailboxID int64
|
|
folder string
|
|
}
|
|
|
|
// Bus fans a Publish out to every current Subscribe-r of the same (mailboxID,
|
|
// folder). Safe for concurrent use.
|
|
type Bus struct {
|
|
mu sync.Mutex
|
|
subs map[key][]chan struct{}
|
|
}
|
|
|
|
func NewBus() *Bus {
|
|
return &Bus{subs: make(map[key][]chan struct{})}
|
|
}
|
|
|
|
// Subscribe returns a channel that receives a value each time Publish(mailboxID,
|
|
// folder) is called, and an unsubscribe func the caller must call when done listening
|
|
// (typically via defer). The channel is buffered 1 and sent to non-blockingly — a
|
|
// subscriber that's slow to drain it can miss a notification, which is fine, since the
|
|
// next poll/idle cycle still reflects real DB state regardless of whether it was
|
|
// notified about this specific change. A nil Bus is a valid receiver (see Publish):
|
|
// the returned channel simply never fires, and unsubscribe is a no-op.
|
|
func (b *Bus) Subscribe(mailboxID int64, folder string) (ch <-chan struct{}, unsubscribe func()) {
|
|
if b == nil {
|
|
return make(chan struct{}), func() {}
|
|
}
|
|
k := key{mailboxID, folder}
|
|
c := make(chan struct{}, 1)
|
|
b.mu.Lock()
|
|
b.subs[k] = append(b.subs[k], c)
|
|
b.mu.Unlock()
|
|
return c, func() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
list := b.subs[k]
|
|
for i, existing := range list {
|
|
if existing == c {
|
|
b.subs[k] = append(list[:i], list[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(b.subs[k]) == 0 {
|
|
delete(b.subs, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Publish wakes every current subscriber of (mailboxID, folder). A nil Bus is a valid
|
|
// no-op receiver — callers that hold an optionally-nil *Bus (e.g. in a test backend
|
|
// that never wired one up) can call Publish unconditionally instead of nil-checking
|
|
// at every call site.
|
|
func (b *Bus) Publish(mailboxID int64, folder string) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
subs := append([]chan struct{}{}, b.subs[key{mailboxID, folder}]...)
|
|
b.mu.Unlock()
|
|
for _, c := range subs {
|
|
select {
|
|
case c <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// PublishAccountWide wakes every subscriber of mailboxID's account-wide sentinel key
|
|
// (folder ""), which never collides with a real folder name. Used by JMAP's
|
|
// EventSource push (internal/jmap), which pushes per-account rather than per-folder
|
|
// like IMAP IDLE/webmail's SSE — call this alongside the normal per-folder Publish at
|
|
// any call site that creates/mutates/deletes a message or folder, so a JMAP client's
|
|
// EventSource connection sees it too.
|
|
func (b *Bus) PublishAccountWide(mailboxID int64) {
|
|
b.Publish(mailboxID, "")
|
|
}
|